<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Ryan Spletzer</title>
    <description>Personal site of Ryan Spletzer</description>
    <link>https://www.spletzer.com/</link>
    <atom:link href="https://www.spletzer.com/feed.xml" rel="self" type="application/rss+xml"/>
    <language>en-us</language>
    <pubDate>Tue, 05 May 2026 23:17:56 +0000</pubDate>
    <lastBuildDate>Tue, 05 May 2026 23:17:56 +0000</lastBuildDate>
    <generator>Jekyll v4.4.1</generator>
    
      <item>
        <title>How to Disable OTP on Your YubiKey</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>That mysterious string of characters someone just pasted into Slack? That&apos;s a YubiKey&apos;s OTP slot firing. Here&apos;s how to disable it via the ykman CLI or YubiKey Manager GUI, and why it won&apos;t affect FIDO2, OpenPGP, or anything else you actually use.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/Pieter_Bruegel_the_Elder_-_The_Tower_of_Babel_(Vienna)_-_Google_Art_Project.jpg&quot; alt=&quot;A massive unfinished stone tower spiraling into the clouds above a hazy landscape, with tiny figures, buildings, and ships visible at its base; the tower&apos;s many arches and tiers are shown in different stages of construction.&quot; /&gt;
&lt;em&gt;Pieter Bruegel the Elder, The Tower of Babel, c. 1563. Kunsthistorisches Museum, Vienna. Public domain,
via
&lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Pieter_Bruegel_the_Elder_-_The_Tower_of_Babel_(Vienna)_-_Google_Art_Project.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’ve spent any amount of time in a Slack workspace
where people use YubiKeys,
you’ve seen it:
suddenly a message appears in a channel that looks something like this:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ccccccgkgdkvvvjvfblbbdihehndrvjkutduvftrgubfc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you’re not familiar with YubiKeys, this looks like gibberish—maybe
someone’s cat walked across the keyboard,
or they fell asleep with their face on the keys.
But if you &lt;em&gt;are&lt;/em&gt; familiar with YubiKeys,
you know exactly what happened:
they bumped the sensor on their key,
and the OTP (One-Time Password) slot fired a
&lt;a href=&quot;https://developers.yubico.com/OTP/&quot;&gt;Yubico OTP&lt;/a&gt; string
directly into whatever text field had focus at the time.&lt;/p&gt;

&lt;p&gt;I have personally witnessed this more times than I can count.
My response is always the same—I
react to the message with the Yubico emoji.&lt;sup id=&quot;fnref:yubico-emoji&quot;&gt;&lt;a href=&quot;#fn:yubico-emoji&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
Some people see my reaction and immediately know what happened.
Others are genuinely confused,
both by the mystery string they just broadcasted
and by my cryptic emoji response.
It’s a near-universal YubiKey user experience,
and it’s entirely preventable.&lt;/p&gt;

&lt;p&gt;If you’re here, you probably want to stop this from happening.
Good news: it takes about 30 seconds.&lt;/p&gt;

&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#quick-reference&quot; id=&quot;markdown-toc-quick-reference&quot;&gt;Quick Reference&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#via-yubikey-manager-gui&quot; id=&quot;markdown-toc-via-yubikey-manager-gui&quot;&gt;Via YubiKey Manager (GUI)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#via-ykman-cli&quot; id=&quot;markdown-toc-via-ykman-cli&quot;&gt;Via ykman (CLI)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#disabling-otp-vs-removing-the-credential&quot; id=&quot;markdown-toc-disabling-otp-vs-removing-the-credential&quot;&gt;Disabling OTP vs. Removing the Credential&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#what-this-doesnt-affect&quot; id=&quot;markdown-toc-what-this-doesnt-affect&quot;&gt;What This Doesn’t Affect&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#re-enabling-otp&quot; id=&quot;markdown-toc-re-enabling-otp&quot;&gt;Re-Enabling OTP&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#what-otp-actually-is-and-why-you-probably-dont-need-it&quot; id=&quot;markdown-toc-what-otp-actually-is-and-why-you-probably-dont-need-it&quot;&gt;What OTP Actually Is (And Why You Probably Don’t Need It)&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#yubico-otp&quot; id=&quot;markdown-toc-yubico-otp&quot;&gt;Yubico OTP&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#static-password&quot; id=&quot;markdown-toc-static-password&quot;&gt;Static Password&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#oath-hotp&quot; id=&quot;markdown-toc-oath-hotp&quot;&gt;OATH HOTP&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#oath-totp-a-common-source-of-confusion&quot; id=&quot;markdown-toc-oath-totp-a-common-source-of-confusion&quot;&gt;OATH TOTP (A Common Source of Confusion)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#why-fido2webauthn-replaced-all-of-this&quot; id=&quot;markdown-toc-why-fido2webauthn-replaced-all-of-this&quot;&gt;Why FIDO2/WebAuthn Replaced All of This&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#conclusion&quot; id=&quot;markdown-toc-conclusion&quot;&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;quick-reference&quot;&gt;Quick Reference&lt;/h2&gt;

&lt;p&gt;If you just want the fix and don’t need the backstory,
here you go.
The GUI app approach is first because that’s what most people will reach for—if
you’re not at home in a terminal,
clicking a checkbox is the friendlier path.
If you do live in a terminal,
scroll past it to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; approach below.&lt;/p&gt;

&lt;h3 id=&quot;via-yubikey-manager-gui&quot;&gt;Via YubiKey Manager (GUI)&lt;/h3&gt;

&lt;p&gt;Yubico provides a graphical application called
&lt;a href=&quot;https://www.yubico.com/support/download/yubikey-manager/&quot;&gt;YubiKey Manager&lt;/a&gt;.
It runs on macOS, Windows, and Linux,
and you can also install it through package managers like Homebrew and Chocolatey.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Open YubiKey Manager and insert your YubiKey&lt;/li&gt;
  &lt;li&gt;Click &lt;strong&gt;Interfaces&lt;/strong&gt; in the left sidebar&lt;/li&gt;
  &lt;li&gt;Under &lt;strong&gt;USB&lt;/strong&gt;, uncheck &lt;strong&gt;OTP&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;If your YubiKey supports NFC, under &lt;strong&gt;NFC&lt;/strong&gt;, uncheck &lt;strong&gt;OTP&lt;/strong&gt; as well&lt;/li&gt;
  &lt;li&gt;Click &lt;strong&gt;Save Interfaces&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done.
The application will confirm the change,
and your YubiKey will no longer emit OTP strings on touch.&lt;/p&gt;

&lt;h3 id=&quot;via-ykman-cli&quot;&gt;Via ykman (CLI)&lt;/h3&gt;

&lt;p&gt;If you’d rather use the command line,
first install the
&lt;a href=&quot;https://developers.yubico.com/yubikey-manager/&quot;&gt;YubiKey Manager CLI&lt;/a&gt;
if you haven’t already:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# macOS&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ykman

&lt;span class=&quot;c&quot;&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; yubikey-manager

&lt;span class=&quot;c&quot;&gt;# Windows (Chocolatey)&lt;/span&gt;
choco &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;yubikey-manager &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Windows (winget)&lt;/span&gt;
winget &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;Yubico.YubiKeyManager
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then, with your YubiKey inserted, disable OTP on the USB interface:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ykman config usb &lt;span class=&quot;nt&quot;&gt;--disable&lt;/span&gt; OTP
USB configuration changes:
  Disable Yubico OTP
  The YubiKey will reboot
Proceed? &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;y/N]: y
USB application configuration updated.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And if your YubiKey has NFC, disable OTP there too:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ykman config nfc &lt;span class=&quot;nt&quot;&gt;--disable&lt;/span&gt; OTP
NFC configuration changes:
  Disable Yubico OTP
  The YubiKey will reboot
Proceed? &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;y/N]: y
NFC application configuration updated.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it.
No more accidental OTP strings.
Read on for more context, and to understand what you just disabled.&lt;/p&gt;

&lt;h2 id=&quot;disabling-otp-vs-removing-the-credential&quot;&gt;Disabling OTP vs. Removing the Credential&lt;/h2&gt;

&lt;p&gt;There are actually two different things you can do here to remove OTP,
and it’s worth understanding the distinction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disabling the OTP application&lt;/strong&gt; is what the Quick Reference section above does.
This turns off the entire OTP interface at the transport level—USB
and/or NFC.
The YubiKey no longer presents an OTP interface to the host at all.
The credential that was in the slot still exists on the key,
but it’s completely inaccessible.
This is the recommended approach if you don’t use OTP for anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removing the slot credential&lt;/strong&gt; is a more surgical option.
Your YubiKey has two OTP slots:
Slot 1 (short touch, 1–2.5 seconds)
and Slot 2 (long touch, 3–5 seconds).
The accidental-OTP problem is almost always Slot 1 firing on a brief touch.
You can remove just that credential while leaving the OTP application enabled:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ykman otp info
Slot 1: programmed
Slot 2: empty

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ykman otp delete 1
Do you really want to delete the configuration of slot 1? &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;y/N]: y
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After this, touching the sensor briefly does nothing—Slot
1 is empty, so there’s no credential to fire.
But the OTP application itself is still active,
which means you could program a new credential into either slot later
if you wanted to.&lt;/p&gt;

&lt;p&gt;You can check the current state of your slots at any time
with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman otp info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to use which:&lt;/strong&gt;
if you never use OTP for anything—and
most people don’t—disable
the entire application.
If you want to keep one slot active
(say, a static password in Slot 2)
but stop the accidental Slot 1 misfires,
just delete the Slot 1 credential.&lt;/p&gt;

&lt;h2 id=&quot;what-this-doesnt-affect&quot;&gt;What This Doesn’t Affect&lt;/h2&gt;

&lt;p&gt;This is the part where I reassure you
that you’re not breaking your YubiKey.&lt;/p&gt;

&lt;p&gt;Disabling OTP has &lt;strong&gt;zero effect&lt;/strong&gt; on any of the other applications
on your key.
YubiKey applications are independent modules,
and turning one off doesn’t touch the others:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;FIDO2/WebAuthn&lt;/strong&gt; — passkeys, hardware security keys for web authentication.
This is what the industry has moved to, and it has nothing to do with OTP.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OpenPGP&lt;/strong&gt; — GPG commit signing, encryption, and authentication.
If you followed my
&lt;a href=&quot;/2026/04/a-no-nonsense-guide-to-gpg-commit-signing-with-a-yubikey/&quot;&gt;GPG commit signing guide&lt;/a&gt;,
none of that is affected.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PIV&lt;/strong&gt; — smart card authentication, certificate-based auth.
Unrelated to OTP.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OATH&lt;/strong&gt; — TOTP and HOTP via the
&lt;a href=&quot;https://www.yubico.com/products/yubico-authenticator/&quot;&gt;Yubico Authenticator&lt;/a&gt; app.
This is a common source of confusion—the
OATH application is a &lt;em&gt;completely separate module&lt;/em&gt;
from the OTP application,
even though the names sound similar.
Disabling OTP does not affect your TOTP codes in Yubico Authenticator.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;re-enabling-otp&quot;&gt;Re-Enabling OTP&lt;/h2&gt;

&lt;p&gt;Disabling OTP is fully reversible.&lt;/p&gt;

&lt;p&gt;If you ever need OTP back—and
I’m struggling to imagine why you would,
but who am I to judge—the
process is straightforward.&lt;/p&gt;

&lt;p&gt;Via the GUI app: same steps as before in the &lt;a href=&quot;#quick-reference&quot;&gt;Quick Reference&lt;/a&gt;,
but check the OTP box instead of unchecking it.&lt;/p&gt;

&lt;p&gt;Via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Re-enable OTP over USB&lt;/span&gt;
ykman config usb &lt;span class=&quot;nt&quot;&gt;--enable&lt;/span&gt; OTP

&lt;span class=&quot;c&quot;&gt;# Re-enable OTP over NFC&lt;/span&gt;
ykman config nfc &lt;span class=&quot;nt&quot;&gt;--enable&lt;/span&gt; OTP
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you went the slot-credential route
and deleted the Yubico OTP credential from Slot 1,
you can reprogram it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;ykman otp yubiotp 1 &lt;span class=&quot;nt&quot;&gt;--serial-public-id&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--generate-private-id&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--generate-key&lt;/span&gt;
Using YubiKey serial as public ID: vvcccbblbgjr
Using a randomly generated private ID: 7d795b01b50c
Using a randomly generated secret key: f857d5634a0cf143b7bf88dd0ee6af92
Program a YubiOTP credential &lt;span class=&quot;k&quot;&gt;in &lt;/span&gt;slot 1? &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;y/N]: y
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This generates a new Yubico OTP credential
and programs it into Slot 1.
Note that this is a &lt;em&gt;new&lt;/em&gt; credential—the
old one is gone—so
you would need to re-register it with any services
that relied on the previous Yubico OTP credential.&lt;sup id=&quot;fnref:re-registration&quot;&gt;&lt;a href=&quot;#fn:re-registration&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-otp-actually-is-and-why-you-probably-dont-need-it&quot;&gt;What OTP Actually Is (And Why You Probably Don’t Need It)&lt;/h2&gt;

&lt;p&gt;If you’re still reading,
you might be curious about what exactly you just turned off,
and why it existed in the first place.
The OTP application on a YubiKey supports several different credential types,
and the terminology can be confusing
because “OTP” gets used to mean different things
in different contexts.&lt;/p&gt;

&lt;h3 id=&quot;yubico-otp&quot;&gt;Yubico OTP&lt;/h3&gt;

&lt;p&gt;Every YubiKey ships with a Yubico OTP credential pre-programmed into Slot 1,
and it’s the thing that fires when you accidentally touch the sensor.&lt;/p&gt;

&lt;p&gt;When the slot fires, the YubiKey types a 44-character string
that encodes the key’s identity and a counter,
encrypted with a symmetric key
that’s shared with
&lt;a href=&quot;https://www.yubico.com/products/yubicloud/&quot;&gt;Yubico’s validation servers (YubiCloud)&lt;/a&gt;.
A service that wants to verify the OTP sends it to YubiCloud,
which decrypts it,
checks the counter to prevent replay attacks,
and responds with a pass or fail.&lt;sup id=&quot;fnref:yubicloud-validation&quot;&gt;&lt;a href=&quot;#fn:yubicloud-validation&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That pre-programmed Slot 1 credential is registered with YubiCloud at the factory—which
is what makes Yubico OTP work out of the box,
but also means the symmetric key exists on Yubico’s servers from day one.
This is fine for the threat model the protocol was designed around back in the day,
but it’s another reason the industry has converged on public-key approaches
like FIDO2 where no copy of your secret lives anywhere but on the key itself.&lt;/p&gt;

&lt;p&gt;This was genuinely useful in the pre-FIDO2 era.
Before WebAuthn became a standard,
Yubico OTP was one of the best options available
for hardware-backed two-factor authentication.
Some services still support it—Yubico’s
own forums being a notable example—but
the list has been shrinking for years
as the industry has converged on FIDO2/WebAuthn.&lt;/p&gt;

&lt;p&gt;The name “YubiKey” itself is a reference to this feature—”Yubi”
comes from “ubiquitous,”
and the original product, released in 2008, was an OTP token only.&lt;/p&gt;

&lt;h3 id=&quot;static-password&quot;&gt;Static Password&lt;/h3&gt;

&lt;p&gt;A YubiKey OTP slot can also be configured to hold a static password—a
fixed string that gets typed every time you touch the sensor.&lt;sup id=&quot;fnref:password-sensitive&quot;&gt;&lt;a href=&quot;#fn:password-sensitive&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;
This is essentially a hardware-stored password.&lt;/p&gt;

&lt;p&gt;It’s the simplest credential type:
no cloud validation, no counters, no encryption.
Just a string that gets typed.
Some people use this as a component of a passphrase—the
YubiKey types a long random portion,
and they type a memorized prefix or suffix.
In practice, this is rarely used,
and FIDO2/WebAuthn is a far better solution for authentication.&lt;/p&gt;

&lt;h3 id=&quot;oath-hotp&quot;&gt;OATH HOTP&lt;/h3&gt;

&lt;p&gt;OTP slots can also hold
HMAC-based One-Time Password (HOTP) credentials,
as defined in &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc4226&quot;&gt;RFC 4226&lt;/a&gt;.
These are counter-based: each touch generates the next code in a sequence.&lt;/p&gt;

&lt;p&gt;This is distinct from the &lt;em&gt;OATH application&lt;/em&gt; on the YubiKey,
which is a separate module that can store
many TOTP and HOTP credentials
and is accessed via the Yubico Authenticator app rather than
through the OTP slot.
If you have an HOTP credential in an OTP slot,
disabling the OTP application &lt;em&gt;will&lt;/em&gt; disable it—but
the OATH application remains unaffected.&lt;/p&gt;

&lt;h3 id=&quot;oath-totp-a-common-source-of-confusion&quot;&gt;OATH TOTP (A Common Source of Confusion)&lt;/h3&gt;

&lt;p&gt;Time-based One-Time Passwords
(&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6238&quot;&gt;RFC 6238&lt;/a&gt;)
&lt;strong&gt;cannot&lt;/strong&gt; be generated via the OTP slots.
The YubiKey lacks a real-time clock,
so it can’t compute time-based codes on its own.&lt;/p&gt;

&lt;p&gt;TOTP is instead handled by the separate &lt;strong&gt;OATH application&lt;/strong&gt;
on the YubiKey.
The YubiKey stores the TOTP secrets,
and the
&lt;a href=&quot;https://www.yubico.com/products/yubico-authenticator/&quot;&gt;Yubico Authenticator&lt;/a&gt; app
on your computer or phone provides the clock
and displays the codes.
This is an entirely different module from OTP—disabling
the OTP application has no effect on your TOTP codes.&lt;/p&gt;

&lt;p&gt;This is probably the single biggest point of confusion around YubiKey OTP.
People hear “I’m disabling OTP” and panic about their authenticator codes.
The OATH application and the OTP application have nothing to do with each other.&lt;/p&gt;

&lt;h3 id=&quot;why-fido2webauthn-replaced-all-of-this&quot;&gt;Why FIDO2/WebAuthn Replaced All of This&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://fidoalliance.org/fido2-2/fido2-web-authentication-webauthn/&quot;&gt;FIDO2/WebAuthn&lt;/a&gt;
is the modern standard for hardware-backed authentication,
and it fixes the problems
that every OTP-based approach has:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Phishing-resistant by design&lt;/strong&gt; — the credential is
&lt;a href=&quot;https://www.w3.org/TR/webauthn-3/#sctn-rp-id&quot;&gt;origin-bound&lt;/a&gt;,
meaning the browser will not submit it to a lookalike domain
that an attacker may have set up.
OTP credentials have no concept of origin—they’re
just strings that get pasted wherever you’re typing,
which is, incidentally, the entire problem this post is about.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No shared secrets&lt;/strong&gt; — FIDO2 uses public-key cryptography.
The private key never leaves the hardware token.
Yubico OTP requires a symmetric key
shared between the YubiKey and YubiCloud,
which means there’s a copy of the secret on Yubico’s servers.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No third-party validation service&lt;/strong&gt; — FIDO2 verification happens
directly between the service you’re logging into and the authenticator.
Yubico OTP requires the service to send the OTP to YubiCloud
for validation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Built into everything&lt;/strong&gt; — every major browser, operating system,
and a rapidly growing number of services support WebAuthn natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;The OTP application on your YubiKey is a legacy feature.
It was useful in its time, but that time has largely passed.
Disabling it doesn’t remove any functionality you’re likely using—it
just stops the key from doing the one thing that annoys you,
and spares your peers from seeing (and making fun of) your gibberish strings.&lt;/p&gt;

&lt;!-- markdownlint-disable-next-line MD022 --&gt;
&lt;h2 class=&quot;no_toc&quot; id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:yubico-emoji&quot;&gt;

      &lt;p&gt;If your Slack workspace doesn’t have a Yubico emoji,
I highly recommend adding one.
It’s the most efficient way to acknowledge
what just happened without having to explain it. &lt;a href=&quot;#fnref:yubico-emoji&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:re-registration&quot;&gt;

      &lt;p&gt;Regenerating the Yubico OTP credential produces fresh secrets—a
new private ID and a new AES key.
Any service that was validating the old credential
won’t recognize the new one,
so you would need to re-register the YubiKey with those services.
This is one more reason to prefer disabling the OTP application
over deleting the slot credential—if
you ever need the credential again,
re-enabling the application restores the original credential
since it was never deleted from the key. &lt;a href=&quot;#fnref:re-registration&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:yubicloud-validation&quot;&gt;

      &lt;p&gt;Yubico OTP validation requires network access to YubiCloud
(or a self-hosted validation server).
This is another reason the protocol has fallen out of favor—it
requires the service to call out to Yubico just to verify the credential.
Yubico does provide
&lt;a href=&quot;https://developers.yubico.com/yubikey-val/&quot;&gt;open-source validation server software&lt;/a&gt;
for self-hosting, but very few organizations go that route. &lt;a href=&quot;#fnref:yubicloud-validation&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:password-sensitive&quot;&gt;

      &lt;p&gt;And you thought emitting a random string into a Slack channel was bad,
imagine doing that with your password. Yikes. &lt;a href=&quot;#fnref:password-sensitive&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sat, 18 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/04/how-to-disable-otp-on-your-yubikey/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/04/how-to-disable-otp-on-your-yubikey/</guid>
        
        <category>security</category>
        
        <category>yubikey</category>
        
        
      </item>
    
      <item>
        <title>A No-Nonsense Guide to GPG Commit Signing with a YubiKey</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>GPG-signed Git commits prove that code actually came from you, and storing your signing key on a YubiKey means the private key never persists on your filesystem. This guide walks through setting it all up on macOS, Windows, and Ubuntu.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/960px-William_Orpen_-_The_Signing_of_Peace_in_the_Hall_of_Mirrors.jpg&quot; alt=&quot;Delegates seated at a long table in the Hall of Mirrors at Versailles, signing the Treaty of Versailles in 1919, with tall arched mirrors and ornate chandeliers reflected behind them.&quot; /&gt;
&lt;em&gt;William Orpen, The Signing of Peace in the Hall of Mirrors, 1919. Imperial War Museum, London. Public domain,
via
&lt;a href=&quot;https://commons.wikimedia.org/wiki/File:William_Orpen_%E2%80%93_The_Signing_of_Peace_in_the_Hall_of_Mirrors.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Anyone who knows me well knows that I nerd out about some specific things,
like &lt;a href=&quot;/2025/11/oidc-oauth-spec-graph/&quot;&gt;OAuth&lt;/a&gt; and its adjacent specs like OpenID Connect, JWT, etc.,
and other auth specs like FIDO2/WebAuthn and SPIFFE/SPIRE,
&lt;a href=&quot;https://git-scm.com/book/en/v2&quot;&gt;Git itself&lt;/a&gt;,&lt;sup id=&quot;fnref:pro-git-book&quot;&gt;&lt;a href=&quot;#fn:pro-git-book&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
CI/CD,
Zero Trust,
certain fancy words like “&lt;a href=&quot;https://en.wikipedia.org/wiki/Idempotence&quot;&gt;idempotency&lt;/a&gt;,”
and in recent years AI and data engineering practices
(just to name a few).&lt;/p&gt;

&lt;p&gt;I’m also a big believer in the promise and evolution of the secure software supply chain,
and am of the firm belief that a secure software supply chain starts with &lt;em&gt;you&lt;/em&gt;,
on your local machine.&lt;/p&gt;

&lt;p&gt;Therefore it should come as no surprise
that I nerd out quite heavily about GPG-signed Git commits—cryptographic
proof that code actually came from &lt;em&gt;you&lt;/em&gt;
(possibly in tandem with an AI agent helping you)—and
about taking that an extra step further
by storing your signing key on a YubiKey&lt;sup id=&quot;fnref:other-hardware-keys&quot;&gt;&lt;a href=&quot;#fn:other-hardware-keys&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;
so that the private key never persists on your filesystem.&lt;/p&gt;

&lt;p&gt;This guide walks through how to set up GPG commit signing with a YubiKey on macOS, Windows, and Ubuntu.&lt;/p&gt;

&lt;p&gt;Now, you may look at this post and think:
“Ryan, this is really long, and it even has a table of contents…
is this truly ‘No-Nonsense’?”&lt;/p&gt;

&lt;p&gt;Trust me when I say this: the nonsense is as minimized as possible here.
Going through GPG / YubiKey setups has traditionally been not well-explained,
and not for the faint of heart
(hence why many people don’t do it!).
Because I’ve been doing this for many years,
I have thought beyond initial setup
and further to the many scenarios you &lt;em&gt;will&lt;/em&gt; run into along the way:
for example, not just how to set up your key initially,
but what you have to do for setting up a second YubiKey,
what you have to do if you need to re-key,
and more.&lt;/p&gt;

&lt;p&gt;So while this post covers the initial setup,
it also serves as a reference you can come back to
when those additional scenarios inevitably arise.
Feel free to jump to the section(s) that are relevant for your situation.
I’ll keep adding to this as I think of more scenarios to address,
so you can refer back to this post when needed, and so I can, too!&lt;sup id=&quot;fnref:unaddressed-scenarios&quot;&gt;&lt;a href=&quot;#fn:unaddressed-scenarios&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;ul id=&quot;markdown-toc&quot;&gt;
  &lt;li&gt;&lt;a href=&quot;#why-bother&quot; id=&quot;markdown-toc-why-bother&quot;&gt;Why Bother?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#overview--tldr&quot; id=&quot;markdown-toc-overview--tldr&quot;&gt;Overview / TL;DR&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#planning-your-key-identity&quot; id=&quot;markdown-toc-planning-your-key-identity&quot;&gt;Planning Your Key Identity&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#prerequisites&quot; id=&quot;markdown-toc-prerequisites&quot;&gt;Prerequisites&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-1-generate-your-gpg-key-pair&quot; id=&quot;markdown-toc-step-1-generate-your-gpg-key-pair&quot;&gt;Step 1: Generate Your GPG Key Pair&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#macos--key-generation&quot; id=&quot;markdown-toc-macos--key-generation&quot;&gt;macOS – Key Generation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#ubuntu--key-generation&quot; id=&quot;markdown-toc-ubuntu--key-generation&quot;&gt;Ubuntu – Key Generation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#windows--key-generation&quot; id=&quot;markdown-toc-windows--key-generation&quot;&gt;Windows – Key Generation&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot; id=&quot;markdown-toc-step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2: Move the Signing Subkey to Your YubiKey&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#change-your-yubikey-pins&quot; id=&quot;markdown-toc-change-your-yubikey-pins&quot;&gt;Change Your YubiKey PINs&lt;/a&gt;        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;#changing-pins-with-ykman-recommended&quot; id=&quot;markdown-toc-changing-pins-with-ykman-recommended&quot;&gt;Changing PINs with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; (Recommended)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#changing-pins-with-gpg---card-edit-alternative&quot; id=&quot;markdown-toc-changing-pins-with-gpg---card-edit-alternative&quot;&gt;Changing PINs with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-edit&lt;/code&gt; (Alternative)&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#ghostty-quirk-screen-or-window-too-small&quot; id=&quot;markdown-toc-ghostty-quirk-screen-or-window-too-small&quot;&gt;Ghostty Quirk: “Screen or Window Too Small”&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;#gui-pinentry-quirk-sorry-no-terminal-at-all-requested&quot; id=&quot;markdown-toc-gui-pinentry-quirk-sorry-no-terminal-at-all-requested&quot;&gt;GUI Pinentry Quirk: “Sorry, No Terminal at All Requested”&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#move-the-subkey-onto-the-card&quot; id=&quot;markdown-toc-move-the-subkey-onto-the-card&quot;&gt;Move the Subkey onto the Card&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#loading-the-same-key-onto-a-second-yubikey&quot; id=&quot;markdown-toc-loading-the-same-key-onto-a-second-yubikey&quot;&gt;Loading the Same Key onto a Second YubiKey&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#removing-the-master-key-from-your-machine&quot; id=&quot;markdown-toc-removing-the-master-key-from-your-machine&quot;&gt;Removing the Master Key from Your Machine&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-3-configure-git-for-gpg-signing&quot; id=&quot;markdown-toc-step-3-configure-git-for-gpg-signing&quot;&gt;Step 3: Configure Git for GPG Signing&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#macos--git-config&quot; id=&quot;markdown-toc-macos--git-config&quot;&gt;macOS – Git Config&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#ubuntu--git-config&quot; id=&quot;markdown-toc-ubuntu--git-config&quot;&gt;Ubuntu – Git Config&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#windows--git-config&quot; id=&quot;markdown-toc-windows--git-config&quot;&gt;Windows – Git Config&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-4-upload-your-public-key-to-github&quot; id=&quot;markdown-toc-step-4-upload-your-public-key-to-github&quot;&gt;Step 4: Upload Your Public Key to GitHub&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#making-your-public-key-easy-to-find-later&quot; id=&quot;markdown-toc-making-your-public-key-easy-to-find-later&quot;&gt;Making Your Public Key Easy to Find Later&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-5-test-it&quot; id=&quot;markdown-toc-step-5-test-it&quot;&gt;Step 5: Test It&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#setting-up-on-a-new-machine&quot; id=&quot;markdown-toc-setting-up-on-a-new-machine&quot;&gt;Setting Up on a New Machine&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#macos--new-machine&quot; id=&quot;markdown-toc-macos--new-machine&quot;&gt;macOS – New Machine&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#ubuntu--new-machine&quot; id=&quot;markdown-toc-ubuntu--new-machine&quot;&gt;Ubuntu – New Machine&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#windows--new-machine&quot; id=&quot;markdown-toc-windows--new-machine&quot;&gt;Windows – New Machine&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#re-keying-updating-your-email-addresses&quot; id=&quot;markdown-toc-re-keying-updating-your-email-addresses&quot;&gt;Re-Keying: Updating Your Email Addresses&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#adding-a-new-uid&quot; id=&quot;markdown-toc-adding-a-new-uid&quot;&gt;Adding a New UID&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#revoking-an-old-uid-optional&quot; id=&quot;markdown-toc-revoking-an-old-uid-optional&quot;&gt;Revoking an Old UID (Optional)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#re-uploading-your-public-key&quot; id=&quot;markdown-toc-re-uploading-your-public-key&quot;&gt;Re-Uploading Your Public Key&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#cleaning-up&quot; id=&quot;markdown-toc-cleaning-up&quot;&gt;Cleaning Up&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#updating-git-config&quot; id=&quot;markdown-toc-updating-git-config&quot;&gt;Updating Git Config&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#revoking-a-compromised-subkey&quot; id=&quot;markdown-toc-revoking-a-compromised-subkey&quot;&gt;Revoking a Compromised Subkey&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#revoke-the-subkey&quot; id=&quot;markdown-toc-revoke-the-subkey&quot;&gt;Revoke the Subkey&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#publish-the-revocation&quot; id=&quot;markdown-toc-publish-the-revocation&quot;&gt;Publish the Revocation&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#generate-a-new-signing-subkey&quot; id=&quot;markdown-toc-generate-a-new-signing-subkey&quot;&gt;Generate a New Signing Subkey&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#load-the-new-subkey-onto-your-yubikeys&quot; id=&quot;markdown-toc-load-the-new-subkey-onto-your-yubikeys&quot;&gt;Load the New Subkey onto Your YubiKey(s)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#update-git-config-and-github&quot; id=&quot;markdown-toc-update-git-config-and-github&quot;&gt;Update Git Config and GitHub&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#clean-up&quot; id=&quot;markdown-toc-clean-up&quot;&gt;Clean Up&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#starting-over-complete-re-key-fresh-start-or-master-key-compromise&quot; id=&quot;markdown-toc-starting-over-complete-re-key-fresh-start-or-master-key-compromise&quot;&gt;Starting Over: Complete Re-Key (Fresh Start or Master Key Compromise)&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#revoke-the-old-key-if-you-still-have-access&quot; id=&quot;markdown-toc-revoke-the-old-key-if-you-still-have-access&quot;&gt;Revoke the Old Key (if You Still Have Access)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#generate-new-keys-and-set-up-from-scratch&quot; id=&quot;markdown-toc-generate-new-keys-and-set-up-from-scratch&quot;&gt;Generate New Keys and Set Up from Scratch&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#updating-other-machines&quot; id=&quot;markdown-toc-updating-other-machines&quot;&gt;Updating Other Machines&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#extending-key-expiration&quot; id=&quot;markdown-toc-extending-key-expiration&quot;&gt;Extending Key Expiration&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#extending-your-master-keys-expiration&quot; id=&quot;markdown-toc-extending-your-master-keys-expiration&quot;&gt;Extending Your Master Key’s Expiration&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#extending-a-subkeys-expiration&quot; id=&quot;markdown-toc-extending-a-subkeys-expiration&quot;&gt;Extending a Subkey’s Expiration&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#after-extending&quot; id=&quot;markdown-toc-after-extending&quot;&gt;After Extending&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#troubleshooting&quot; id=&quot;markdown-toc-troubleshooting&quot;&gt;Troubleshooting&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#gpg-signing-failed-no-secret-key&quot; id=&quot;markdown-toc-gpg-signing-failed-no-secret-key&quot;&gt;“gpg: signing failed: No secret key”&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gpg-signing-failed-inappropriate-ioctl-for-device&quot; id=&quot;markdown-toc-gpg-signing-failed-inappropriate-ioctl-for-device&quot;&gt;“gpg: signing failed: Inappropriate ioctl for device”&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#pin-entry-dialog-doesnt-appear-on-macos&quot; id=&quot;markdown-toc-pin-entry-dialog-doesnt-appear-on-macos&quot;&gt;PIN Entry Dialog Doesn’t Appear on macOS&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gpg-keeps-prompting-for-pin-even-with-the-yubikey-inserted&quot; id=&quot;markdown-toc-gpg-keeps-prompting-for-pin-even-with-the-yubikey-inserted&quot;&gt;GPG Keeps Prompting for PIN Even With the YubiKey Inserted&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#gpg-cant-find-keys-after-upgrading-to-gnupg-24&quot; id=&quot;markdown-toc-gpg-cant-find-keys-after-upgrading-to-gnupg-24&quot;&gt;GPG Can’t Find Keys After Upgrading to GnuPG 2.4+&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#please-insert-card-with-serial-number&quot; id=&quot;markdown-toc-please-insert-card-with-serial-number&quot;&gt;“Please Insert Card with Serial Number…”&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#yubikey-not-detected-after-removing-and-reinserting&quot; id=&quot;markdown-toc-yubikey-not-detected-after-removing-and-reinserting&quot;&gt;YubiKey Not Detected After Removing and Reinserting&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#scdaemon-and-pcscd-conflict-linux&quot; id=&quot;markdown-toc-scdaemon-and-pcscd-conflict-linux&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; Conflict (Linux)&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#wrong-gpg-binary-in-git&quot; id=&quot;markdown-toc-wrong-gpg-binary-in-git&quot;&gt;Wrong &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg&lt;/code&gt; Binary in Git&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#accidental-otp-output-when-touching-yubikey&quot; id=&quot;markdown-toc-accidental-otp-output-when-touching-yubikey&quot;&gt;Accidental OTP Output When Touching YubiKey&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#unusable-secret-key-expired-subkey&quot; id=&quot;markdown-toc-unusable-secret-key-expired-subkey&quot;&gt;“Unusable Secret Key” (Expired Subkey)&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#bonus-using-your-yubikey-for-ssh-authentication&quot; id=&quot;markdown-toc-bonus-using-your-yubikey-for-ssh-authentication&quot;&gt;Bonus: Using Your YubiKey for SSH Authentication&lt;/a&gt;    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;#setup&quot; id=&quot;markdown-toc-setup&quot;&gt;Setup&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;#verifying-it-works&quot; id=&quot;markdown-toc-verifying-it-works&quot;&gt;Verifying It Works&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#closing-thoughts&quot; id=&quot;markdown-toc-closing-thoughts&quot;&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-bother&quot;&gt;Why Bother?&lt;/h2&gt;

&lt;p&gt;If you’ve ever looked at at someone’s activity on GitHub and noticed the green “Verified” badge
across all the commits on their PR,
that’s GPG commit signing at work.
Without it, anyone can set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.name&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.email&lt;/code&gt; in their Git config to whatever they want—though
it would be coming from a different GitHub account,
there’s really nothing stopping someone from “committing as you”
with this commit metadata set in their Git config.&lt;sup id=&quot;fnref:commit-impersonation&quot;&gt;&lt;a href=&quot;#fn:commit-impersonation&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Signing commits with GPG attaches a cryptographic proof to each commit
asserting that it came from the holder of a specific private key.&lt;sup id=&quot;fnref:gpg-hash-then-sign&quot;&gt;&lt;a href=&quot;#fn:gpg-hash-then-sign&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;
(And it also personally gives me a large dopamine hit when I see the green “Verified” badge.)&lt;/p&gt;

&lt;p&gt;If that private key lives on a hardware token like a YubiKey,
it can’t be exfiltrated by malware or accidentally copied—the
signing operation happens on the YubiKey itself,
and you confirm it by entering your YubiKey PIN for the first signing.
After that, the YubiKey stays unlocked for subsequent signings
until you log out, shutdown/reboot, or remove the YubiKey.&lt;sup id=&quot;fnref:touch-to-sign&quot;&gt;&lt;a href=&quot;#fn:touch-to-sign&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I would be remiss in talking about software supply chain
if I didn’t mention that GPG isn’t the only way to sign commits.
GitHub also shows the green “Verified” badge for commits signed with
&lt;a href=&quot;https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification&quot;&gt;SSH or S/MIME&lt;/a&gt;,
and it’s worth being aware of how
&lt;a href=&quot;https://docs.sigstore.dev/cosign/signing/gitsign/&quot;&gt;Sigstore’s Gitsign&lt;/a&gt;
uses OpenID Connect to create keyless signatures
that are strongly tied to your identity without requiring long-lived keys at all.
Certain organizations may be at the level of maturity where Sigstore + OIDC
is the right fit for showing provenance,
but everyone is on their own software supply chain journey
and organizations and people are at different points in that journey.
Notably, GitHub doesn’t currently show the green “Verified” label for Sigstore signatures—which
my dopamine would be sad about—but
Sigstore does provide strong cryptographic assurances through its transparency log.
It’s also worth noting that Git only supports one signing method at a time,
so you have to choose between GPG, SSH, S/MIME, or Sigstore—you
can’t layer them.
In the absence of other options being available to you,
I recommend setting up GPG commit signing yourself,
because it is entirely within your control
and is also what the Linux and Git open source projects use for signing themselves.
GPG signing is better than nothing.
If you are someone who prefers using SSH with GitHub,
then SSH signing may be more appealing for you.&lt;sup id=&quot;fnref:ssh-signing&quot;&gt;&lt;a href=&quot;#fn:ssh-signing&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;overview--tldr&quot;&gt;Overview / TL;DR&lt;/h2&gt;

&lt;p&gt;The setup involves three parts:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Generating a GPG key pair&lt;/strong&gt; – generating a master key (certify) with subkeys for signing, encryption,
and optionally authentication&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Moving to the YubiKey&lt;/strong&gt; – moving the signing subkey gets onto the hardware token so it doesn’t persist on disk&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Configuring Git&lt;/strong&gt; – telling Git to use GPG and point it at your signing subkey&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The end result: every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit&lt;/code&gt; triggers a signing operation on your YubiKey,
and the commit gets a cryptographic signature that GitHub (or any verifier)
can check against your public key.&lt;/p&gt;

&lt;p&gt;I’ll break things down as we go between macOS, Linux (Ubuntu)&lt;sup id=&quot;fnref:other-distros&quot;&gt;&lt;a href=&quot;#fn:other-distros&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;, and Windows.&lt;/p&gt;

&lt;h2 id=&quot;planning-your-key-identity&quot;&gt;Planning Your Key Identity&lt;/h2&gt;

&lt;p&gt;Before you generate anything,
it’s worth understanding a constraint that will shape your setup:
a YubiKey’s OpenPGP applet holds exactly one key slot each
for signing, encryption, and authentication.
That’s one identity per YubiKey—you
can’t load a second, separate GPG key alongside the first.&lt;/p&gt;

&lt;p&gt;If you use multiple email addresses—say,
a personal email address and a work email address—you
have two options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One key, multiple UIDs (recommended):&lt;/strong&gt;
Add all your email addresses as UIDs on a single GPG key.
Git and GitHub match the commit’s author email against the UIDs on your key
to decide whether to show the “Verified” badge,
so as long as every address you commit with is listed as a UID,
you’re covered.&lt;sup id=&quot;fnref:gpg-email-matching&quot;&gt;&lt;a href=&quot;#fn:gpg-email-matching&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;
This is what I do—one GPG key with both my personal and work emails attached,
loaded onto one pair of YubiKeys (primary + secondary).
It’s simpler to manage,
and you only need one set of hardware tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Separate keys per identity:&lt;/strong&gt;
Generate a completely independent GPG key for each email/identity,
each on its own YubiKey (ideally a pair of YubiKeys per GPG identity, for redundancy).
This gives you full isolation between identities—revoking
your work key doesn’t touch your personal one—but
it means more hardware to buy and manage,
separate Git signing configs per repo or directory or machine,
and separate public keys to upload to GitHub.&lt;/p&gt;

&lt;p&gt;For most people, the single-key approach is the pragmatic choice.
The separate-keys approach makes more sense
if your organization absolutely requires dedicated hardware tokens,
or if you just personally want an absolute firewall between identities.&lt;/p&gt;

&lt;p&gt;If you ever need to change an email address down the road—a
new job, a domain change, etc.—you
add the new UID to your existing key, optionally revoke the old one,
and re-upload your public key to GitHub.
I cover that process in
&lt;a href=&quot;#re-keying-updating-your-email-addresses&quot;&gt;Re-Keying: Updating Your Email Addresses&lt;/a&gt; below.&lt;/p&gt;

&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;

&lt;p&gt;Before diving in, you’ll need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;At least one (but very preferably two) &lt;a href=&quot;https://www.yubico.com/products/&quot;&gt;YubiKey&lt;/a&gt;(s) that supports OpenPGP
(YubiKey 5 series or newer is recommended, but older YubiKey 4 works too;
I use a pair of &lt;a href=&quot;https://www.yubico.com/product/yubikey-5c-fips/&quot;&gt;YubiKey 5C FIPS&lt;/a&gt; models)&lt;sup id=&quot;fnref:yubikey-accessories&quot;&gt;&lt;a href=&quot;#fn:yubikey-accessories&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;10&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;A computer with a USB port
(which depends on your YubiKey model,
but on modern computers I’d opt for USB Type-C if you can)&lt;/li&gt;
  &lt;li&gt;Some comfort with the command line&lt;sup id=&quot;fnref:windows-cmd&quot;&gt;&lt;a href=&quot;#fn:windows-cmd&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;11&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;step-1-generate-your-gpg-key-pair&quot;&gt;Step 1: Generate Your GPG Key Pair&lt;/h2&gt;

&lt;p&gt;If you don’t already have a GPG key pair, you’ll need to generate one.
If you already have one, you can skip ahead to
&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2: Move the Signing Subkey to Your YubiKey&lt;/a&gt;,
but you may want to keep reading to compare your generation method with this one.&lt;/p&gt;

&lt;p&gt;The recommended approach is to generate a master key with &lt;strong&gt;Certify&lt;/strong&gt; capability only,
then add separate subkeys for &lt;strong&gt;Sign&lt;/strong&gt;, &lt;strong&gt;Encrypt&lt;/strong&gt;, and optionally &lt;strong&gt;Authenticate&lt;/strong&gt;.
This way, if a subkey is ever compromised, you can revoke just that subkey without losing your
entire identity.&lt;/p&gt;

&lt;p&gt;This guide recommends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519&lt;/code&gt; for all keys.
It’s the modern default—smaller keys, faster signing, and a simpler implementation
with fewer knobs to misconfigure compared to RSA.
YubiKey 5 series supports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519&lt;/code&gt; natively.&lt;sup id=&quot;fnref:rsa-still-fine&quot;&gt;&lt;a href=&quot;#fn:rsa-still-fine&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;12&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h3 id=&quot;macos--key-generation&quot;&gt;macOS – Key Generation&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GnuPG via Homebrew&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;gnupg

&lt;span class=&quot;c&quot;&gt;# Or build from source: https://www.gnupg.org/download/&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Generate a master key with ed25519&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# The interactive prompts will walk you through this&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--full-generate-key&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--expert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;During the interactive prompts, you’ll see a numbered menu.
There are two paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick path – option (9) “ECC and ECC”:&lt;/strong&gt;
creates a master key that can certify and sign,
plus an encryption subkey, all in one step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clean path – option (11) “ECC (set your own capabilities)”:&lt;/strong&gt;
lets you toggle capabilities individually so you can create a certify-only master key,
then add dedicated Sign, Encrypt, and Auth subkeys afterward.&lt;/p&gt;

&lt;p&gt;I recommend the clean path (option 11)&lt;sup id=&quot;fnref:subkey-separation&quot;&gt;&lt;a href=&quot;#fn:subkey-separation&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;13&lt;/a&gt;&lt;/sup&gt;,
but option 9 is perfectly fine if you want to get going quickly.&lt;/p&gt;

&lt;p&gt;Whichever you choose, the remaining prompts are the same:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Select &lt;strong&gt;Curve 25519&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;Set an expiration—I
recommend 2 years for subkeys and 5–10 years for the master key.
Shorter subkey expirations act as a safety net
(if you lose access, the key auto-expires rather than lingering forever),
and you can always
&lt;a href=&quot;#extending-key-expiration&quot;&gt;extend the expiration&lt;/a&gt; later
without generating new keys&lt;/li&gt;
  &lt;li&gt;Enter your name and email address—use
the same email address you commit with in Git
(i.e. the one in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git config user.email&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;Set a strong passphrase—make
it long (I’d say 20+ characters), unique, and stored in your password manager.
This passphrase protects your key backups;
if someone obtains your exported key files,
the passphrase is the only thing standing between them and your identity.
Day-to-day signing uses your YubiKey PIN, not this passphrase,
so you won’t be typing it often—only
during key management operations&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you chose option (11), add your signing subkey now:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Replace YOUR_MASTER_KEY_ID with your master key ID from the output above&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--expert&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# In the GPG prompt:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; addkey&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose (10) ECC (sign only), select Curve 25519, set expiration&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can also add encryption and authentication subkeys the same way if you want those capabilities
on your YubiKey.&lt;/p&gt;

&lt;h3 id=&quot;ubuntu--key-generation&quot;&gt;Ubuntu – Key Generation&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GnuPG&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt update &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; gnupg2 scdaemon pcscd

&lt;span class=&quot;c&quot;&gt;# Generate keys the same way as macOS&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--full-generate-key&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--expert&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The process is identical to macOS from here (see above)—add subkeys with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --expert --edit-key&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;windows--key-generation&quot;&gt;Windows – Key Generation&lt;/h3&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GPG4Win via Chocolatey&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Or download from https://www.gpg4win.org/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;choco&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gpg4win&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Generate keys from a terminal (Git Bash, PowerShell, or cmd)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpg&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--full-generate-key&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--expert&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Same interactive process as macOS above for key generation and subkey creation.&lt;/p&gt;

&lt;h2 id=&quot;step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2: Move the Signing Subkey to Your YubiKey&lt;/h2&gt;

&lt;p&gt;This is the critical step—once you move a subkey to the YubiKey,
&lt;strong&gt;it is removed from your local keyring&lt;/strong&gt;.
The local keyring will retain a “stub” that points to the YubiKey,
but the actual private key material only exists on the hardware token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back up your keys first.&lt;/strong&gt; Seriously.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export your full key (public + private) to a safe location&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--export-secret-keys&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/master-secret-key.asc
gpg &lt;span class=&quot;nt&quot;&gt;--export-secret-subkeys&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/secret-subkeys.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Also generate a revocation certificate now, while you have easy access to the master key:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--gen-revoke&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/revocation-certificate.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A revocation certificate is a pre-signed statement that says “this key is no longer valid.”
If you ever lose access to your master key entirely—lose
the secure backup, forget the passphrase,
or worse, a third party obtains control of the key and passphrase—this
certificate is your only way to tell the world the key is dead.
It’s tied to your key’s fingerprint, not its expiration date,
so it never needs to be regenerated
(not when you extend expiration, add UIDs, or make any other key changes).&lt;/p&gt;

&lt;p&gt;Note that GnuPG 2.1+ actually generates a revocation certificate automatically
during key creation and stores it in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/openpgp-revocs.d/&lt;/code&gt;,
but you should copy it into your secure backup location alongside your key exports
rather than relying on it being on a single machine.&lt;/p&gt;

&lt;p&gt;Move these exported files into a secure storage location,
then delete the local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/gpg-export/&lt;/code&gt; directory.
These files should not persist on your filesystem—the
whole point is that the private key material lives on your YubiKey, not on disk.&lt;/p&gt;

&lt;p&gt;Some practical storage options:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;As file attachments in a password manager like 1Password or Bitwarden
(the keys are already passphrase-protected, and you likely trust your
password manager with everything else already)&lt;/li&gt;
  &lt;li&gt;An encrypted USB drive stored in a fireproof safe or safety deposit box&lt;/li&gt;
  &lt;li&gt;An encrypted archive in a cloud drive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You’ll need these backups if your YubiKey is ever lost or broken.&lt;/p&gt;

&lt;h3 id=&quot;change-your-yubikey-pins&quot;&gt;Change Your YubiKey PINs&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Thanks to Samuel Imfeld for pointing out to me
that I should add a section on changing the PINs!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Before any key material lands on the card,
insert your YubiKey and lock down its default PINs.
Out of the box, your YubiKey’s OpenPGP applet ships with two default PINs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;User PIN&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;123456&lt;/code&gt;) — entered during everyday operations
like commit signing, SSH authentication, or decryption.
Minimum 6 characters on modern YubiKeys; 6 digits is typical.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Admin PIN&lt;/strong&gt; (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;12345678&lt;/code&gt;) — entered for administrative operations
like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keytocard&lt;/code&gt;,
changing the user PIN,
changing card metadata,
or unblocking a locked user PIN.
Minimum 8 characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Setting your own PINs first means the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keytocard&lt;/code&gt; step below
is authorized with a credential only you know,
not the factory-default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;12345678&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A third optional credential, the &lt;strong&gt;Reset Code&lt;/strong&gt;,
lets you unblock the user PIN without needing the admin PIN,
but most people don’t use it—the
admin PIN can also unblock the user PIN,
so I skip setting a reset code.&lt;/p&gt;

&lt;p&gt;I use an 8-digit admin PIN and a 6-digit user PIN,
both stored in my password manager.
Numeric PINs are the most common choice
and what most YubiKey-aware tools and docs assume;
alphanumeric PINs are supported on recent YubiKey 5 series firmware,
but sticking with numeric PINs keeps you compatible
with any hardware pinpad you might encounter later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;These PINs are separate from your FIDO2/WebAuthn PIN.&lt;/strong&gt;
The OpenPGP applet, the FIDO2 applet, and the PIV applet
each have their own independent PIN slots on the YubiKey.
Changing your OpenPGP PIN does not affect your FIDO2 PIN used for passkeys,&lt;sup id=&quot;fnref:fido2-passkey-pin&quot;&gt;&lt;a href=&quot;#fn:fido2-passkey-pin&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;14&lt;/a&gt;&lt;/sup&gt;
and vice versa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PIN retry counters matter here.&lt;/strong&gt;
Entering the wrong user PIN 3 times in a row locks the user PIN—you’ll
then need the admin PIN to unlock it.
Entering the wrong admin PIN 3 times in a row locks the admin PIN too,
at which point the &lt;em&gt;only&lt;/em&gt; recovery path is
a full factory reset of the OpenPGP applet
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman openpgp reset&lt;/code&gt;),
which wipes all keys on the card.
This is why I recommend storing both PINs in a password manager
the moment you set them—forgetting
them means starting over with a fresh &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keytocard&lt;/code&gt; operation
(assuming you still have your master key backup;
if not, it means generating entirely new keys).&lt;/p&gt;

&lt;h4 id=&quot;changing-pins-with-ykman-recommended&quot;&gt;Changing PINs with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; (Recommended)&lt;/h4&gt;

&lt;p&gt;The most reliable way to change your PINs is with
&lt;a href=&quot;https://developers.yubico.com/yubikey-manager/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt;&lt;/a&gt;,
which talks directly to the YubiKey’s smart-card interface
without routing through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install ykman if you haven&apos;t already&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# macOS:&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ykman
&lt;span class=&quot;c&quot;&gt;# Ubuntu:&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; yubikey-manager
&lt;span class=&quot;c&quot;&gt;# Windows:&lt;/span&gt;
choco &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;yubikey-manager &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Change the user PIN (prompts for current PIN, then new PIN twice)&lt;/span&gt;
ykman openpgp access change-pin

&lt;span class=&quot;c&quot;&gt;# Change the admin PIN (prompts for current admin PIN, then new admin PIN twice)&lt;/span&gt;
ykman openpgp access change-admin-pin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; prompts directly in the terminal it’s invoked from,
so it works consistently across Ghostty, Apple Terminal, iTerm2, Windows Terminal, etc.,
without any pinentry configuration to fight with.
For anyone who’s already configured a GUI pinentry like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt;
for day-to-day commit signing,
using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; for PIN changes sidesteps the quirks covered below.&lt;/p&gt;

&lt;h4 id=&quot;changing-pins-with-gpg---card-edit-alternative&quot;&gt;Changing PINs with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-edit&lt;/code&gt; (Alternative)&lt;/h4&gt;

&lt;p&gt;You can also change PINs through GPG directly:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--card-edit&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# At the gpg/card&amp;gt; prompt:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg/card&amp;gt; admin&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg/card&amp;gt; passwd&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose 1 to change the User PIN&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose 3 to change the Admin PIN&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose Q to quit the passwd menu&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg/card&amp;gt; quit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This works, but how the prompts get displayed
depends on whichever pinentry program you have configured—and
that’s where the quirks come in.&lt;/p&gt;

&lt;h4 id=&quot;ghostty-quirk-screen-or-window-too-small&quot;&gt;Ghostty Quirk: “Screen or Window Too Small”&lt;/h4&gt;

&lt;p&gt;If you’re using &lt;a href=&quot;https://ghostty.org/&quot;&gt;Ghostty&lt;/a&gt; as your terminal on macOS
and try to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-edit&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;passwd&lt;/code&gt;
(or any other operation that invokes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-curses&lt;/code&gt;,
such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keytocard&lt;/code&gt; itself),
you may see:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg: KEYTOCARD failed: Screen or window too small
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This appears to be a bug where Ghostty doesn’t fully propagate its window size
to the spawned pinentry process,
so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-curses&lt;/code&gt; refuses to draw its dialog.
The simplest workaround is to run the affected command
from a terminal that reports its size correctly—I’ve
had no trouble using stock &lt;strong&gt;Apple Terminal&lt;/strong&gt; or &lt;strong&gt;iTerm2&lt;/strong&gt;
for these one-off operations,
then returning to Ghostty for day-to-day work.
Ghostty itself works fine for commit signing once a GUI pinentry
(like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt;) is in place;
the issue only surfaces with terminal-resident pinentry variants.&lt;/p&gt;

&lt;p&gt;You can also skip the issue entirely by using
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman openpgp access change-pin&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;change-admin-pin&lt;/code&gt;,
which don’t invoke pinentry at all.&lt;/p&gt;

&lt;h4 id=&quot;gui-pinentry-quirk-sorry-no-terminal-at-all-requested&quot;&gt;GUI Pinentry Quirk: “Sorry, No Terminal at All Requested”&lt;/h4&gt;

&lt;p&gt;If you’ve already configured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-program&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/gpg-agent.conf&lt;/code&gt;
to point at a GUI pinentry (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-gnome3&lt;/code&gt;)
and set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-tty&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/gpg.conf&lt;/code&gt;,
then try to change your PIN with
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-edit&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;admin&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;passwd&lt;/code&gt;,
you may see something like:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg: OpenPGP card no. D27600012401... detected
gpg: Sorry, no terminal at all requested - can&apos;t get input
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This happens because the card-edit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;passwd&lt;/code&gt; flow
expects to display its own “choose 1/3/Q” menu in the terminal
(separate from the PIN entry prompt itself),
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-tty&lt;/code&gt; combined with a GUI-only pinentry
leaves GPG with nowhere to show that menu.&lt;/p&gt;

&lt;p&gt;Two ways around it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; instead&lt;/strong&gt; —
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman openpgp access change-pin&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman openpgp access change-admin-pin&lt;/code&gt;
don’t rely on pinentry or GPG’s interactive card-edit,
so they’re unaffected by this.
This is my preferred path.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Temporarily revert your pinentry settings&lt;/strong&gt; —
comment out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-program&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/gpg-agent.conf&lt;/code&gt;,
comment out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-tty&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/gpg.conf&lt;/code&gt;,
restart the agent with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpgconf --kill gpg-agent&lt;/code&gt;,
perform the PIN change,
then restore the settings and restart the agent again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The upshot:
your GUI pinentry configuration is oriented around &lt;em&gt;signing&lt;/em&gt;—where
you want a dialog that works regardless of which process triggered the commit—but
PIN-management flows in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-edit&lt;/code&gt;
expect a real TTY to display their own interactive menu.
Decoupling PIN management from GPG entirely,
via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt;,
avoids having to choose between the two configurations.&lt;/p&gt;

&lt;h3 id=&quot;move-the-subkey-onto-the-card&quot;&gt;Move the Subkey onto the Card&lt;/h3&gt;

&lt;p&gt;With your PINs locked down,
move the signing subkey onto the card:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Edit your key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Select the signing subkey (check the index -- usually key 1 or 2)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; key 1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; keytocard&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose (1) Signature key&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can verify the key is on the card:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should see your signing key fingerprint listed under “Signature key”
and your subkey listing should show &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssb&amp;gt;&lt;/code&gt; (the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;&lt;/code&gt; indicates the key is on a card).&lt;/p&gt;

&lt;h3 id=&quot;loading-the-same-key-onto-a-second-yubikey&quot;&gt;Loading the Same Key onto a Second YubiKey&lt;/h3&gt;

&lt;p&gt;I strongly recommend having two YubiKeys:
a primary YubiKey that you carry and a secondary YubiKey stored somewhere safe
(I keep mine in a fireproof safe at home).
This is good practice for FIDO2/WebAuthn&lt;sup id=&quot;fnref:fido2-passkeys&quot;&gt;&lt;a href=&quot;#fn:fido2-passkeys&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;15&lt;/a&gt;&lt;/sup&gt; as well
(where you’d pre-register both keys with your services),
and it applies equally to GPG signing.&lt;/p&gt;

&lt;p&gt;When my primary YubiKey was stolen
(along with my backpack—more post-mortem learnings from that to come in a future blog post!),
I was very glad I had a secondary ready to go.&lt;sup id=&quot;fnref:stolen-yubikey-risk&quot;&gt;&lt;a href=&quot;#fn:stolen-yubikey-risk&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;16&lt;/a&gt;&lt;/sup&gt;
Without it, I would have left hanging while I waited for a new YubiKey to arrive
(not to mention since I rely on it for sign-in for many services, beyond just the GPG use case),
and further without secure backups of the GPG master and sub keys
I would have to generate entirely new keys,
re-upload them to GitHub, and update every machine I use.&lt;/p&gt;

&lt;p&gt;The simplest approach is to load the &lt;em&gt;same&lt;/em&gt; signing subkey onto both YubiKeys.
This way your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitconfig&lt;/code&gt; doesn’t change regardless of which key is plugged in—Git
just sees the same subkey ID either way.
The tradeoff is that you can’t revoke one without revoking the other
(since it’s the same subkey),
but since the private key can’t be extracted from a stolen YubiKey,
this is unlikely to matter in practice.&lt;/p&gt;

&lt;p&gt;To do this, &lt;strong&gt;before you discard your local key backup&lt;/strong&gt; from the step above,
move the subkey to your second YubiKey:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# After moving the subkey to your first YubiKey and saving,&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# restore the local private key from your backup&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--delete-secret-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/master-secret-key.asc

&lt;span class=&quot;c&quot;&gt;# Insert your second YubiKey and move the subkey onto it&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; key 1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; keytocard&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose (1) Signature key&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After this, both YubiKeys hold the same signing subkey,
and you can swap between them seamlessly.
Just remember that when you plug in a different YubiKey than the one GPG last saw,
you may need to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-status&lt;/code&gt; so GPG re-discovers the key stub
on the new card.&lt;/p&gt;

&lt;h3 id=&quot;removing-the-master-key-from-your-machine&quot;&gt;Removing the Master Key from Your Machine&lt;/h3&gt;

&lt;p&gt;Once your subkeys are on your YubiKey(s) and your backups are safely stored offline,
there’s no reason to keep the master key’s private portion on your local keyring.
You only need the master key for infrequent key management tasks—adding
a new UID, extending the expiration date, revoking a subkey,
or signing someone else’s key.
For day-to-day commit signing, the subkey on your YubiKey is all you need.
Assuming you performed all the correct secure backup steps for your master and subkeys
mentioned during key generation above,
you are free to safely remove the master key from your keyring.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Remove the secret master key from your local keyring&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--delete-secret-keys&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Re-import the public key so GPG still knows about your key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc

&lt;span class=&quot;c&quot;&gt;# Plug in your YubiKey so GPG re-discovers the subkey stubs&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After this, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --list-secret-keys&lt;/code&gt; will show &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sec#&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sec&lt;/code&gt;—the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;#&lt;/code&gt; indicates the master key’s private portion is absent.
Your signing subkey on the YubiKey still works normally.&lt;/p&gt;

&lt;p&gt;When you eventually need to do key management,
temporarily import your master key from your secure backup,
do the work, then delete it again.
This is elaborated on in several scenarios described later in this blog post.&lt;/p&gt;

&lt;h2 id=&quot;step-3-configure-git-for-gpg-signing&quot;&gt;Step 3: Configure Git for GPG Signing&lt;/h2&gt;

&lt;h3 id=&quot;macos--git-config&quot;&gt;macOS – Git Config&lt;/h3&gt;

&lt;p&gt;The macOS setup involves a few pieces:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install pinentry-mac for the PIN prompt dialog&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or build from source: https://github.com/GPGTools/pinentry-mac&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;pinentry-mac
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Configure GPG to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; for PIN entry—this
gives you a native macOS dialog instead of a terminal prompt:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
pinentry-program /opt/homebrew/bin/pinentry-mac
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re on an Intel Mac, the path would be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/bin/pinentry-mac&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;This is worth calling out: using a GUI pinentry like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; instead of the text-based
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-curses&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-tty&lt;/code&gt; isn’t just a cosmetic preference—it’s
a practical requirement if you use AI coding tools like
&lt;a href=&quot;https://docs.anthropic.com/en/docs/claude-code/overview&quot;&gt;Claude Code&lt;/a&gt;
that execute Git commands on your behalf.
I used to use the text-based pinentry in my terminal,
but when Claude Code runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit&lt;/code&gt; it spawns the process in a way where the terminal-based
pinentry can’t grab the TTY to prompt you for your PIN.
With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt;, the PIN dialog pops open as a native macOS window regardless of which process
triggered the commit, so signing works seamlessly whether you’re committing manually or through an
AI assistant.&lt;/p&gt;

&lt;p&gt;You may also want to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-tty&lt;/code&gt; in your GPG config (which I personally do),
which further ensures GPG doesn’t try to interact with the terminal directly—this
complements the GUI pinentry approach:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg.conf
no-tty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re using the newer keyboxd (GnuPG 2.4+), add this (which I also personally do):&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/common.conf
use-keyboxd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPG_TTY&lt;/code&gt; environment variable in your shell RC file
(needed even with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; as a fallback):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Add to ~/.zshrc or ~/.bashrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GPG_TTY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tty&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for fish, add to ~/.config/fish/config.fish&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set -x GPG_TTY (tty)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for PowerShell, add to your $PROFILE&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# $env:GPG_TTY = (tty)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now configure Git:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Point Git at your GPG binary, homebrew path shown below&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# If you installed via a different method this may be a different path&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program /opt/homebrew/bin/gpg

&lt;span class=&quot;c&quot;&gt;# Set your signing key (use the signing *subkey* ID, not the master key)&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.signingKey YOUR_SIGNING_SUBKEY_ID

&lt;span class=&quot;c&quot;&gt;# Enable signing for all commits&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; commit.gpgsign &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;ubuntu--git-config&quot;&gt;Ubuntu – Git Config&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install a GUI pinentry (recommended) or terminal pinentry&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; pinentry-gnome3
&lt;span class=&quot;c&quot;&gt;# Or for KDE: sudo apt install -y pinentry-qt&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or terminal-only: sudo apt install -y pinentry-curses&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Add to ~/.bashrc or ~/.zshrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GPG_TTY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tty&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for fish, add to ~/.config/fish/config.fish&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set -x GPG_TTY (tty)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Configure Git&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program gpg
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.signingKey YOUR_SIGNING_SUBKEY_ID
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; commit.gpgsign &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent.conf&lt;/code&gt; pinentry line,
I recommend a GUI pinentry on desktop Ubuntu for the same reason
as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; on macOS—if
you use AI coding tools like Claude Code that run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git commit&lt;/code&gt; on your behalf,
a terminal-based pinentry like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-curses&lt;/code&gt; can’t grab the TTY
to prompt for your PIN.
A GUI pinentry pops up a dialog window regardless of which process triggered the commit.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
pinentry-program /usr/bin/pinentry-gnome3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or for KDE:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pinentry-program /usr/bin/pinentry-qt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re on a headless server with no desktop environment,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-curses&lt;/code&gt; is your only option:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pinentry-program /usr/bin/pinentry-curses
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You may also want to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no-tty&lt;/code&gt; in your GPG config,
just like on macOS, to ensure GPG doesn’t try to interact with the terminal directly:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg.conf
no-tty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re using GnuPG 2.4+ with keyboxd:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/common.conf
use-keyboxd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;windows--git-config&quot;&gt;Windows – Git Config&lt;/h3&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# If using GPG4Win, Kleopatra handles PIN entry automatically&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Configure Git (in Git Bash or PowerShell)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gpg.program&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;C:\Program Files (x86)\GnuPG\bin\gpg.exe&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user.signingKey&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;YOUR_SIGNING_SUBKEY_ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;commit.gpgsign&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you installed GPG4Win, the Kleopatra application manages PIN entry via a GUI dialog.
This works well with AI coding tools like Claude Code for the same reason
as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; on macOS and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-gnome3&lt;/code&gt; on Ubuntu—the
PIN dialog pops up as a native window regardless of which process triggered the commit.
If you installed GPG standalone via Chocolatey, you may need to configure pinentry separately.&lt;/p&gt;

&lt;h2 id=&quot;step-4-upload-your-public-key-to-github&quot;&gt;Step 4: Upload Your Public Key to GitHub&lt;/h2&gt;

&lt;p&gt;For GitHub to show the “Verified” badge, it needs your public key:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export your public key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--export&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Copy the output (including the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-----BEGIN PGP PUBLIC KEY BLOCK-----&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-----END PGP PUBLIC KEY BLOCK-----&lt;/code&gt; lines) and add it in GitHub under
&lt;strong&gt;Settings &amp;gt; SSH and GPG keys &amp;gt; New GPG key&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This step is the same regardless of your operating system.&lt;/p&gt;

&lt;h3 id=&quot;making-your-public-key-easy-to-find-later&quot;&gt;Making Your Public Key Easy to Find Later&lt;/h3&gt;

&lt;p&gt;While you’re generating your public key,
this is a good time to make sure you can easily retrieve it
when &lt;a href=&quot;#setting-up-on-a-new-machine&quot;&gt;setting up a new machine&lt;/a&gt; down the road.&lt;/p&gt;

&lt;p&gt;You can &lt;em&gt;optionally&lt;/em&gt; upload it to a public keyserver:&lt;sup id=&quot;fnref:keys-openpgp-org&quot;&gt;&lt;a href=&quot;#fn:keys-openpgp-org&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;17&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then on any new machine, you can import it directly:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--recv-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alternatively, keep your public key (not your private key!)
in a place you can easily access—a
private GitHub gist, a cloud drive, or even committed to your dotfiles repo (potentially),
but honestly what I do is just keep this in my password manager, too, for convenience.&lt;/p&gt;

&lt;h2 id=&quot;step-5-test-it&quot;&gt;Step 5: Test It&lt;/h2&gt;

&lt;p&gt;Make a test commit and verify it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create a test commit&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;--allow-empty&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Test GPG signing&quot;&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Verify the signature&lt;/span&gt;
git log &lt;span class=&quot;nt&quot;&gt;--show-signature&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You should see output indicating a good signature from your key.
If your YubiKey is plugged in, you’ll be prompted for your PIN.&lt;/p&gt;

&lt;p&gt;If your YubiKey is &lt;strong&gt;not&lt;/strong&gt; plugged in, the commit will fail
and/or the pinentry terminal prompt or GUI will indicate you need to insert your card—this
is the intended behavior, because the private key only exists on the hardware token.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-on-a-new-machine&quot;&gt;Setting Up on a New Machine&lt;/h2&gt;

&lt;p&gt;Getting your initial key generated and moved onto the YubiKey is a one-time thing.
But when you get a new machine—or reinstall your OS—you
need to get the new machine to recognize the key that’s already on your YubiKey.
This is the part that tripped me up the first time,
because the steps are different from the initial setup and not always well-documented.&lt;/p&gt;

&lt;p&gt;The core idea is the same on every platform:
install GPG, import your public key, plug in the YubiKey so GPG discovers the private key stubs,
set trust, and configure Git and pinentry.&lt;/p&gt;

&lt;p&gt;One step you’ll notice here that wasn’t needed during initial setup is setting trust.
When you generate a key, GPG automatically assigns “ultimate” trust to it
because it knows you created it yourself.
When you import a key on a new machine,
GPG doesn’t automatically know it’s &lt;em&gt;yours&lt;/em&gt;—it
just sees an imported public key—so
you need to explicitly tell GPG to trust it.
Without this, GPG will warn you on every signing operation.&lt;/p&gt;

&lt;p&gt;If you uploaded your public key to a keyserver
&lt;a href=&quot;#making-your-public-key-easy-to-find-later&quot;&gt;earlier&lt;/a&gt;,
importing it on the new machine is one command:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--recv-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Otherwise, import it from a file
(downloaded from your password manager, cloud drive, etc.)
or export it from another machine that already has it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# From a file&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;macos--new-machine&quot;&gt;macOS – New Machine&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GnuPG and pinentry-mac&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or build from source: https://www.gnupg.org/download/&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or build from source: https://github.com/GPGTools/pinentry-mac&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;gnupg pinentry-mac

&lt;span class=&quot;c&quot;&gt;# Import your public key (from a backup, a keyserver, or export from another machine)&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc

&lt;span class=&quot;c&quot;&gt;# Plug in your YubiKey and tell GPG to discover the private key stubs on the card&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Trust your own key (otherwise GPG will warn on every signature)&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; trust&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose 5 (ultimate)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then set up the same config files as in
&lt;a href=&quot;#step-3-configure-git-for-gpg-signing&quot;&gt;Step 3&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
pinentry-program /opt/homebrew/bin/pinentry-mac
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg.conf
no-tty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/common.conf (GnuPG 2.4+)
use-keyboxd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Add to ~/.zshrc or ~/.bashrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GPG_TTY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tty&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for fish, add to ~/.config/fish/config.fish&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set -x GPG_TTY (tty)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for PowerShell, add to your $PROFILE&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# $env:GPG_TTY = (tty)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Configure Git&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# If you installed via a different method this may be a different path&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program /opt/homebrew/bin/gpg
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.signingKey YOUR_SIGNING_SUBKEY_ID
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; commit.gpgsign &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;ubuntu--new-machine&quot;&gt;Ubuntu – New Machine&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GnuPG and smartcard support with a GUI pinentry&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt update &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; gnupg2 scdaemon pcscd pinentry-gnome3
&lt;span class=&quot;c&quot;&gt;# Or for KDE: replace pinentry-gnome3 with pinentry-qt&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Or headless: replace pinentry-gnome3 with pinentry-curses&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Import your public key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc

&lt;span class=&quot;c&quot;&gt;# Plug in your YubiKey and discover the private key stubs&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Trust your own key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; trust&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose 5 (ultimate)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then configure the same config files as in
&lt;a href=&quot;#step-3-configure-git-for-gpg-signing&quot;&gt;Step 3&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
pinentry-program /usr/bin/pinentry-gnome3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg.conf
no-tty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/common.conf (GnuPG 2.4+)
use-keyboxd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Add to ~/.bashrc or ~/.zshrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;GPG_TTY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;tty&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for fish, add to ~/.config/fish/config.fish&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set -x GPG_TTY (tty)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Configure Git&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program gpg
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.signingKey YOUR_SIGNING_SUBKEY_ID
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; commit.gpgsign &lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;windows--new-machine&quot;&gt;Windows – New Machine&lt;/h3&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install GPG4Win&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Or download from https://www.gpg4win.org/&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;choco&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gpg4win&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Import your public key (from PowerShell, Git Bash, or cmd)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpg&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;~/gpg-export/public-key.asc&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Plug in your YubiKey and discover the private key stubs&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpg&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Trust your own key&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gpg&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;YOUR_MASTER_KEY_ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; trust&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Choose 5 (ultimate)&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Configure Git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;gpg.program&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;C:\Program Files (x86)\GnuPG\bin\gpg.exe&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;user.signingKey&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;YOUR_SIGNING_SUBKEY_ID&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;git&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;commit.gpgsign&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Kleopatra (included with GPG4Win) handles PIN entry automatically on Windows,
so no separate pinentry configuration is needed.&lt;/p&gt;

&lt;h2 id=&quot;re-keying-updating-your-email-addresses&quot;&gt;Re-Keying: Updating Your Email Addresses&lt;/h2&gt;

&lt;p&gt;At some point you’ll likely need to update the email addresses on your GPG key—you
switch jobs, your company changes its domain,
or you retire an old personal address.
Because your YubiKey holds your &lt;em&gt;subkeys&lt;/em&gt; (signing, encryption, authentication)
and UIDs live on the &lt;em&gt;master key&lt;/em&gt;,
the good news is that re-keying doesn’t require touching the YubiKey at all.
It’s purely a master-key operation.&lt;/p&gt;

&lt;p&gt;You’ll need your master key’s private portion for this,
so temporarily import it from your secure backup
(from your password manager or wherever you’ve securely stored it):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Import the master key from your secure backup&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/master-secret-key.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;adding-a-new-uid&quot;&gt;Adding a New UID&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# In the GPG prompt:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; adduid&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Enter your new name and email address&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;revoking-an-old-uid-optional&quot;&gt;Revoking an Old UID (Optional)&lt;/h3&gt;

&lt;p&gt;If the old address is no longer valid and you don’t want it associated with your key,
you can revoke it.
A revoked UID stays visible on the key (GPG doesn’t truly delete UIDs)
but is marked as no longer valid:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Select the UID to revoke (UIDs are numbered starting at 1)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; uid 2&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; revuid&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Confirm the revocation&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’d rather keep the old UID active—maybe
the address still works as an alias—that’s fine too.
Either way, historical commits aren’t affected:
GitHub stores verification records at push time,
so commits that were already verified keep their “Verified” badge
regardless of UID revocations.&lt;/p&gt;

&lt;h3 id=&quot;re-uploading-your-public-key&quot;&gt;Re-Uploading Your Public Key&lt;/h3&gt;

&lt;p&gt;After modifying UIDs, you need to re-export and re-upload your public key
so verifiers (like GitHub) know about the change:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export the updated public key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--export&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/public-key.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then replace the key in GitHub under
&lt;strong&gt;Settings &amp;gt; SSH and GPG keys&lt;/strong&gt;—add
the new export first, then delete the old entry.
GitHub stores a
&lt;a href=&quot;https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification&quot;&gt;verification record&lt;/a&gt;
at push time, so previously-verified commits keep their “Verified” badge
even after you rotate or revoke keys.&lt;/p&gt;

&lt;p&gt;If you use a keyserver, push the update there too:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;cleaning-up&quot;&gt;Cleaning Up&lt;/h3&gt;

&lt;p&gt;Once you’re done, remove the master key’s private portion from your local keyring again
(just like in
&lt;a href=&quot;#removing-the-master-key-from-your-machine&quot;&gt;Removing the master key from your machine&lt;/a&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--delete-secret-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;And don’t forget to update your secure backup with the new export,
since the master key now has updated UIDs.&lt;/p&gt;

&lt;h3 id=&quot;updating-git-config&quot;&gt;Updating Git Config&lt;/h3&gt;

&lt;p&gt;If your new email address is the one you want to commit with going forward,
update your Git config:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.email &lt;span class=&quot;s2&quot;&gt;&quot;your-new-email@example.com&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No change is needed for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.signingKey&lt;/code&gt;—that
points to your signing subkey, which hasn’t changed.&lt;/p&gt;

&lt;h2 id=&quot;revoking-a-compromised-subkey&quot;&gt;Revoking a Compromised Subkey&lt;/h2&gt;

&lt;p&gt;To be clear: if your YubiKey is lost or stolen,
your signing subkey is almost certainly &lt;em&gt;not&lt;/em&gt; compromised.
The private key cannot be extracted from the hardware token,
and the PIN retry counter locks the card after 3 failed attempts.
A lost YubiKey is not analogous to a leaked private key file.&lt;/p&gt;

&lt;p&gt;That said, if you believe a &lt;em&gt;subkey&lt;/em&gt; was compromised—for
example, your subkey backup was obtained by a third party
along with the passphrase—you
can revoke just that subkey and generate a new one
while keeping your master key and identity intact.&lt;/p&gt;

&lt;p&gt;If the compromise extends to your &lt;em&gt;master key&lt;/em&gt;
(your secure backup was exposed along with the passphrase),
use the revocation certificate you generated in
&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2&lt;/a&gt; and follow the
&lt;a href=&quot;#starting-over-complete-re-key-fresh-start-or-master-key-compromise&quot;&gt;Starting Over: Complete Re-Key (Fresh Start or Master Key Compromise)&lt;/a&gt;
process instead.&lt;/p&gt;

&lt;h3 id=&quot;revoke-the-subkey&quot;&gt;Revoke the Subkey&lt;/h3&gt;

&lt;p&gt;Import your master key from your secure backup,
then revoke the compromised signing subkey:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Import the master key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/master-secret-key.asc

&lt;span class=&quot;c&quot;&gt;# Edit the key and revoke the signing subkey&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Select the compromised subkey (check the index)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; key 1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; revkey&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Confirm the revocation and provide a reason&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;publish-the-revocation&quot;&gt;Publish the Revocation&lt;/h3&gt;

&lt;p&gt;The revocation needs to reach anyone who might verify your signatures:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export the updated public key (now containing the revocation)&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--export&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/public-key.asc

&lt;span class=&quot;c&quot;&gt;# Push to keyserver if you use one&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On GitHub, go to &lt;strong&gt;Settings &amp;gt; SSH and GPG keys&lt;/strong&gt;,
add the updated public key export (which includes the revocation metadata),
then remove the old entry.
Because GitHub stores verification records at push time,
your previously-verified commits keep their “Verified” badge—the
revocation prevents &lt;em&gt;new&lt;/em&gt; signatures from being verified under the old subkey,
but doesn’t retroactively invalidate past ones.&lt;/p&gt;

&lt;h3 id=&quot;generate-a-new-signing-subkey&quot;&gt;Generate a New Signing Subkey&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Still in edit mode with the master key imported&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--expert&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; addkey&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Choose (10) ECC (sign only), select Curve 25519, set expiration&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;load-the-new-subkey-onto-your-yubikeys&quot;&gt;Load the New Subkey onto Your YubiKey(s)&lt;/h3&gt;

&lt;p&gt;Follow the same process as
&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2: Move the Signing Subkey to Your YubiKey&lt;/a&gt;—move
the new subkey to your primary YubiKey,
then restore from backup and move to your secondary if you have one.&lt;/p&gt;

&lt;h3 id=&quot;update-git-config-and-github&quot;&gt;Update Git Config and GitHub&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Point Git at the new signing subkey ID&lt;/span&gt;
git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; user.signingKey YOUR_NEW_SIGNING_SUBKEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Upload the new public key export to GitHub
(the one containing both the revoked old subkey and the new active one).
Future commits will be signed with the new subkey and show “Verified.”&lt;/p&gt;

&lt;h3 id=&quot;clean-up&quot;&gt;Clean Up&lt;/h3&gt;

&lt;p&gt;First, export your updated public key
(which now contains the revoked old subkey and the new active one)
and update your secure backup:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export the updated public key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--export&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/public-key.asc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then remove the master key’s private portion from your local keyring.
The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--delete-secret-keys&lt;/code&gt; command removes all secret key material,
so you re-import the public key afterward
so GPG still knows your key exists
(and the YubiKey stubs get re-associated when you run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--card-status&lt;/code&gt;):&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--delete-secret-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Test a signed commit&lt;/span&gt;
git commit &lt;span class=&quot;nt&quot;&gt;--allow-empty&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Test new signing subkey&quot;&lt;/span&gt;
git log &lt;span class=&quot;nt&quot;&gt;--show-signature&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;starting-over-complete-re-key-fresh-start-or-master-key-compromise&quot;&gt;Starting Over: Complete Re-Key (Fresh Start or Master Key Compromise)&lt;/h2&gt;

&lt;p&gt;The sections above cover lighter-weight changes—updating
UIDs or revoking a single subkey while keeping the same master key.
Sometimes you need to start from scratch with an entirely new GPG identity.&lt;/p&gt;

&lt;p&gt;Scenarios where a complete re-key makes sense:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your &lt;strong&gt;master key&lt;/strong&gt; was compromised (not just a subkey)—practically
this means someone obtained your key material along with the passphrase,
or obtained your key material and your passphrase was weak enough to be guessed&lt;/li&gt;
  &lt;li&gt;You want to &lt;strong&gt;switch algorithms&lt;/strong&gt; (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RSA 4096&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ed25519&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;You’ve &lt;strong&gt;lost both YubiKeys and your secure backup&lt;/strong&gt;—you
have no way to recover the old key&lt;/li&gt;
  &lt;li&gt;Your key has &lt;strong&gt;expired&lt;/strong&gt; and you’d rather start fresh
than extend it&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;revoke-the-old-key-if-you-still-have-access&quot;&gt;Revoke the Old Key (if You Still Have Access)&lt;/h3&gt;

&lt;p&gt;If you generated a revocation certificate during
&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Step 2&lt;/a&gt;
(or GnuPG generated one for you in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/openpgp-revocs.d/&lt;/code&gt;),
this is the fastest path—you
don’t need the master key’s private portion at all:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Import the pre-generated revocation certificate&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/revocation-certificate.asc

&lt;span class=&quot;c&quot;&gt;# Publish the revoked key to keyserver&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_OLD_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Alternatively, if you have the master key but not the revocation certificate,
you can generate one now:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Import the master key from your secure backup&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/master-secret-key.asc

&lt;span class=&quot;c&quot;&gt;# Generate a revocation certificate&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--gen-revoke&lt;/span&gt; YOUR_OLD_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; revocation-certificate.asc

&lt;span class=&quot;c&quot;&gt;# Import the revocation into your keyring&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; revocation-certificate.asc

&lt;span class=&quot;c&quot;&gt;# Publish the revoked key to keyserver&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_OLD_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On GitHub, you can leave the old (now-revoked) key in place or remove it.
Previously-verified commits keep their “Verified” badge regardless.&lt;/p&gt;

&lt;p&gt;If you’ve lost access to both the master key &lt;em&gt;and&lt;/em&gt; the revocation certificate,
you can’t revoke it—just
remove the old public key from GitHub and move on.
Previously-verified commits still keep their “Verified” badge,
and the old key will eventually expire on its own
(assuming you set an expiration date, which is one more reason to always set one).&lt;/p&gt;

&lt;h3 id=&quot;generate-new-keys-and-set-up-from-scratch&quot;&gt;Generate New Keys and Set Up from Scratch&lt;/h3&gt;

&lt;p&gt;From here, the process is the same as a first-time setup.
Walk through each step with your new key:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#step-1-generate-your-gpg-key-pair&quot;&gt;Generate your new GPG key pair&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-2-move-the-signing-subkey-to-your-yubikey&quot;&gt;Move the signing subkey to your YubiKey(s)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-3-configure-git-for-gpg-signing&quot;&gt;Configure Git&lt;/a&gt;—update
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user.signingKey&lt;/code&gt; to point to your new signing subkey ID&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-4-upload-your-public-key-to-github&quot;&gt;Upload your new public key to GitHub&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#step-5-test-it&quot;&gt;Test it&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don’t forget to store your new master key and subkey backups securely,
just like the first time around.&lt;/p&gt;

&lt;h3 id=&quot;updating-other-machines&quot;&gt;Updating Other Machines&lt;/h3&gt;

&lt;p&gt;Any machine that was configured with the old key will need to be updated.
Follow the &lt;a href=&quot;#setting-up-on-a-new-machine&quot;&gt;Setting Up on a New Machine&lt;/a&gt; steps,
importing the &lt;em&gt;new&lt;/em&gt; public key instead of the old one.&lt;/p&gt;

&lt;p&gt;You’ll also want to clean out the old key from those machines:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Remove the old key from your keyring&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--delete-keys&lt;/span&gt; YOUR_OLD_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;extending-key-expiration&quot;&gt;Extending Key Expiration&lt;/h2&gt;

&lt;p&gt;Expiration dates on GPG keys aren’t permanent—they’re
metadata that can be updated at any time,
as long as you have the master key’s private portion.
This applies to both the master key &lt;em&gt;and&lt;/em&gt; subkeys.&lt;/p&gt;

&lt;p&gt;This is one of the reasons shorter subkey expirations (like 2 years) are practical:
you’re not committing to a hard deadline,
just setting a safety net that you periodically push forward.&lt;/p&gt;

&lt;h3 id=&quot;extending-your-master-keys-expiration&quot;&gt;Extending Your Master Key’s Expiration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Import your master key from your secure backup&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/master-secret-key.asc

&lt;span class=&quot;c&quot;&gt;# Edit the key (the master key is selected by default)&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; expire&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Enter the new expiration period (e.g. 5y for 5 years from today)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;extending-a-subkeys-expiration&quot;&gt;Extending a Subkey’s Expiration&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--edit-key&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Select the subkey you want to extend (check the index)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; key 1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; expire&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Enter the new expiration period (e.g. 2y for 2 years from today)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# gpg&amp;gt; save&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;You can extend multiple subkeys in one session—select
each one with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key N&lt;/code&gt;, run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;expire&lt;/code&gt;, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save&lt;/code&gt; when you’re done.&lt;/p&gt;

&lt;h3 id=&quot;after-extending&quot;&gt;After Extending&lt;/h3&gt;

&lt;p&gt;Once you’ve updated expiration dates,
re-export and re-upload your public key
so that verifiers know about the new dates:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Export the updated public key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--armor&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--export&lt;/span&gt; YOUR_MASTER_KEY_ID &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; ~/gpg-export/public-key.asc

&lt;span class=&quot;c&quot;&gt;# Push to keyserver if you use one&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--keyserver&lt;/span&gt; keys.openpgp.org &lt;span class=&quot;nt&quot;&gt;--send-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On GitHub, add the updated export under
&lt;strong&gt;Settings &amp;gt; SSH and GPG keys&lt;/strong&gt;, then remove the old entry.&lt;/p&gt;

&lt;p&gt;Then clean up: remove the master key’s private portion from your local keyring,
and update your secure backup with the new export.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--delete-secret-keys&lt;/span&gt; YOUR_MASTER_KEY_ID
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No changes are needed to your YubiKey, Git config, or signing subkey ID—the
subkeys themselves haven’t changed, just their expiration metadata.&lt;/p&gt;

&lt;h2 id=&quot;troubleshooting&quot;&gt;Troubleshooting&lt;/h2&gt;

&lt;p&gt;A few common issues and fixes:&lt;/p&gt;

&lt;h3 id=&quot;gpg-signing-failed-no-secret-key&quot;&gt;“gpg: signing failed: No secret key”&lt;/h3&gt;

&lt;p&gt;This usually means GPG can’t find the key stub pointing to your YubiKey.
Try:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Restart the GPG agent&lt;/span&gt;
gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; gpg-agent

&lt;span class=&quot;c&quot;&gt;# Re-read the card&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gpg-signing-failed-inappropriate-ioctl-for-device&quot;&gt;“gpg: signing failed: Inappropriate ioctl for device”&lt;/h3&gt;

&lt;p&gt;This means &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPG_TTY&lt;/code&gt; isn’t set. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;export GPG_TTY=$(tty)&lt;/code&gt; to your shell RC file and restart
your terminal.&lt;/p&gt;

&lt;h3 id=&quot;pin-entry-dialog-doesnt-appear-on-macos&quot;&gt;PIN Entry Dialog Doesn’t Appear on macOS&lt;/h3&gt;

&lt;p&gt;Make sure &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; is installed and configured in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/gpg-agent.conf&lt;/code&gt;.
Then restart the agent:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; gpg-agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gpg-keeps-prompting-for-pin-even-with-the-yubikey-inserted&quot;&gt;GPG Keeps Prompting for PIN Even With the YubiKey Inserted&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Thanks again to Samuel Imfeld
for pointing out the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent&lt;/code&gt; cache-ttl mitigation below!)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The YubiKey only treats the User PIN as “verified”
for the duration of the current &lt;em&gt;card session&lt;/em&gt;,
and that session can drop without you ever unplugging the key.
Common causes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Sleep/wake&lt;/strong&gt; — on setups where system suspend disrupts USB
or forces re-enumeration on wake,
the card session ends and the next signing operation opens a fresh one.
Whether your particular machine does this depends on OS,
hardware, and power-management config.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PIV use deselects OpenPGP&lt;/strong&gt; — OpenPGP and PIV share the YubiKey’s CCID
smart-card interface, and only one applet is “selected” at a time.
An SSH client using PIV via PKCS#11,
or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman piv&lt;/code&gt; command, will switch the selected applet to PIV,
which clears OpenPGP’s PIN-verified flag per the OpenPGP card spec.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; conflicts on Linux&lt;/strong&gt; — see
&lt;a href=&quot;#scdaemon-and-pcscd-conflict-linux&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; Conflict (Linux)&lt;/a&gt; below.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent&lt;/code&gt; restart&lt;/strong&gt; — manual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpgconf --kill gpg-agent&lt;/code&gt;,
a system update that swaps the binary, or a crash wipes the agent’s state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mitigation is to extend gpg-agent’s PIN cache window
so you only enter the PIN once per work session.
gpg-agent caches the User PIN alongside passphrases
and silently re-supplies it whenever the card asks for re-verification—both
when the card session has reset &lt;em&gt;and&lt;/em&gt;,
on YubiKeys with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forcesig&lt;/code&gt; turned on,
on every individual signing operation.
The defaults are 10 minutes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default-cache-ttl&lt;/code&gt;) and 2 hours (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max-cache-ttl&lt;/code&gt;).
You can bump them:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
default-cache-ttl 28800
max-cache-ttl 86400
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s 8 hours and 24 hours respectively—roughly “one PIN per workday.”
Apply and reload:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; gpg-agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A note on why some readers never hit this symptom in the first place:
if your YubiKey has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forcesig&lt;/code&gt; off
(which lets PW1 stay verified across signatures
until the OpenPGP applet is deselected)
&lt;em&gt;and&lt;/em&gt; your card session is staying alive between signs,
the card simply isn’t asking for re-verification,
so the cache TTL is moot.
You’ll still be prompted after unplugging the YubiKey
or restarting the machine,
but not in between—regardless of what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default-cache-ttl&lt;/code&gt; is set to.
You can check your own setting in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-status&lt;/code&gt;—the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Signature PIN&lt;/code&gt; line reads either &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;forced&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;not forced&lt;/code&gt;.
Separately on macOS,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pinentry-mac&lt;/code&gt; has a “Save in Keychain” checkbox
that persists the User PIN in your login keychain across reboots—a
different security tradeoff than the in-memory cache,
worth knowing about but distinct from gpg-agent’s TTL.&lt;/p&gt;

&lt;p&gt;The tradeoff for bumping cache-ttl:
an unlocked, unattended machine can sign on your behalf for as long as the cache is warm.
If you’ve enabled touch-to-sign,&lt;sup id=&quot;fnref:touch-to-sign:1&quot;&gt;&lt;a href=&quot;#fn:touch-to-sign&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;
that concern goes away—every signature still needs a physical YubiKey tap
regardless of cache state.&lt;/p&gt;

&lt;h3 id=&quot;gpg-cant-find-keys-after-upgrading-to-gnupg-24&quot;&gt;GPG Can’t Find Keys After Upgrading to GnuPG 2.4+&lt;/h3&gt;

&lt;p&gt;If GPG stops finding your keys after an upgrade
(e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --list-keys&lt;/code&gt; shows nothing, or signing fails with “No secret key”),
you may need to enable keyboxd.
GnuPG 2.4+ uses a new key storage backend (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyboxd&lt;/code&gt;)
that replaces the older &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pubring.kbx&lt;/code&gt; file,
but it won’t activate unless you opt in:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/common.conf
use-keyboxd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;After adding this, restart the agent and re-import your public key:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; gpg-agent
gpg &lt;span class=&quot;nt&quot;&gt;--import&lt;/span&gt; ~/gpg-export/public-key.asc
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;please-insert-card-with-serial-number&quot;&gt;“Please Insert Card with Serial Number…”&lt;/h3&gt;

&lt;p&gt;This often means your YubiKey isn’t plugged in.&lt;/p&gt;

&lt;p&gt;However it can also mean or you’ve swapped to a different YubiKey
than the one GPG last saw,
perhaps your secondary one.
GPG remembers the serial number of the last card
and asks for that specific one.
If you’ve swapped Yubikeys, force GPG to re-learn the current card:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg-connect-agent &lt;span class=&quot;s2&quot;&gt;&quot;scd serialno&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;learn --force&quot;&lt;/span&gt; /bye
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;yubikey-not-detected-after-removing-and-reinserting&quot;&gt;YubiKey Not Detected After Removing and Reinserting&lt;/h3&gt;

&lt;p&gt;If you unplug and replug your YubiKey and GPG stops recognizing it
(signing fails or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --card-status&lt;/code&gt; errors),
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt; process has likely cached the old connection and doesn’t notice the card came back.
Kill it and let GPG restart it automatically:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; scdaemon
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;scdaemon-and-pcscd-conflict-linux&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; Conflict (Linux)&lt;/h3&gt;

&lt;p&gt;On Linux, both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt;’s built-in CCID driver and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; service
try to claim exclusive access to the YubiKey.
Symptoms include “card not found” errors or intermittent failures.
The fix is to tell &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scdaemon&lt;/code&gt; to back off and let &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pcscd&lt;/code&gt; handle the hardware:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/scdaemon.conf
disable-ccid
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then restart:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; scdaemon
gpg &lt;span class=&quot;nt&quot;&gt;--card-status&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;wrong-gpg-binary-in-git&quot;&gt;Wrong &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg&lt;/code&gt; Binary in Git&lt;/h3&gt;

&lt;p&gt;If signing fails silently or Git seems to use a different keyring than expected,
Git may be pointing at the wrong GPG binary.
This is common on Windows (where Git ships its own GPG 2.2.x alongside GPG4Win)
and on macOS (where multiple installations can coexist via Homebrew, MacPorts, or GPG Suite).&lt;/p&gt;

&lt;p&gt;Check what Git is using:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then verify it’s the right one:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Should show your keys and YubiKey stubs&lt;/span&gt;
&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;git config &lt;span class=&quot;nt&quot;&gt;--global&lt;/span&gt; gpg.program&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--list-secret-keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If it shows nothing or the wrong keyring, update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg.program&lt;/code&gt;
to point at the correct binary
(see the platform-specific sections in
&lt;a href=&quot;#step-3-configure-git-for-gpg-signing&quot;&gt;Step 3&lt;/a&gt;).&lt;/p&gt;

&lt;h3 id=&quot;accidental-otp-output-when-touching-yubikey&quot;&gt;Accidental OTP Output When Touching YubiKey&lt;/h3&gt;

&lt;p&gt;If you accidentally touch your YubiKey’s sensor and a long string of characters
gets typed into your terminal or editor,
that’s the OTP (One-Time Password) slot firing.
It’s harmless but annoying.
You can disable OTP mode entirely if you don’t use it.
This requires &lt;a href=&quot;https://developers.yubico.com/yubikey-manager/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman&lt;/code&gt; (YubiKey Manager CLI)&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install ykman&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# macOS:&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ykman
&lt;span class=&quot;c&quot;&gt;# Ubuntu:&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apt &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; yubikey-manager
&lt;span class=&quot;c&quot;&gt;# Windows:&lt;/span&gt;
choco &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;yubikey-manager &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Disable OTP&lt;/span&gt;
ykman config usb &lt;span class=&quot;nt&quot;&gt;--disable&lt;/span&gt; OTP
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This leaves the other interfaces (FIDO2, OpenPGP, PIV) unaffected.&lt;/p&gt;

&lt;h3 id=&quot;unusable-secret-key-expired-subkey&quot;&gt;“Unusable Secret Key” (Expired Subkey)&lt;/h3&gt;

&lt;p&gt;If GPG gives a generic “unusable secret key” error during signing,
your signing subkey may have expired.
GPG doesn’t always clearly indicate that expiration is the issue.&lt;/p&gt;

&lt;p&gt;Check your key’s expiration dates:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpg &lt;span class=&quot;nt&quot;&gt;--list-keys&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--with-colons&lt;/span&gt; YOUR_MASTER_KEY_ID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Look for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exp&lt;/code&gt; (expired) in the output.
If your signing subkey has expired,
follow the &lt;a href=&quot;#extending-key-expiration&quot;&gt;Extending Key Expiration&lt;/a&gt; section
to renew it from your master key.&lt;/p&gt;

&lt;h2 id=&quot;bonus-using-your-yubikey-for-ssh-authentication&quot;&gt;Bonus: Using Your YubiKey for SSH Authentication&lt;/h2&gt;

&lt;p&gt;If you added an authentication subkey to your YubiKey during key generation,
you can use that same YubiKey for SSH authentication—meaning
one hardware token handles both GPG commit signing and SSH access.&lt;/p&gt;

&lt;p&gt;The way this works is that the GPG agent can act as an SSH agent.
When you &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh&lt;/code&gt; into a server or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; over SSH,
the GPG agent intercepts the request and uses the authentication subkey
on your YubiKey to perform the handshake.
The SSH private key never exists as a file on disk,
just like your signing key.&lt;/p&gt;

&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;

&lt;p&gt;The steps below work on macOS, Linux, and Git Bash on Windows.
The native Windows &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh.exe&lt;/code&gt; doesn’t use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_AUTH_SOCK&lt;/code&gt;—it
communicates via a named pipe (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\\.\pipe\openssh-ssh-agent&lt;/code&gt;) instead.
Bridging &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent&lt;/code&gt;’s SSH interface to that named pipe is possible
(tools like &lt;a href=&quot;https://github.com/BusyJay/gpg-bridge&quot;&gt;gpg-bridge&lt;/a&gt; exist),
but the ecosystem here is pretty immature.
If you primarily use Windows, you may find it simpler
to use a separate SSH key on your YubiKey via FIDO2/resident keys
rather than routing SSH through GPG.&lt;/p&gt;

&lt;p&gt;First, tell the GPG agent to offer SSH support.
Add this to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg-agent.conf&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# ~/.gnupg/gpg-agent.conf
enable-ssh-support
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then point your shell’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH_AUTH_SOCK&lt;/code&gt; at the GPG agent’s socket
instead of the default SSH agent:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Add to ~/.zshrc or ~/.bashrc&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;export &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;SSH_AUTH_SOCK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--list-dirs&lt;/span&gt; agent-ssh-socket&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Or for fish, add to ~/.config/fish/config.fish&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# set -x SSH_AUTH_SOCK (gpgconf --list-dirs agent-ssh-socket)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Next, tell the GPG agent which key to offer for SSH.
Each GPG key has a “keygrip”—a hash that identifies it independently of the key ID.
You need to add your authentication subkey’s keygrip to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.gnupg/sshcontrol&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Find the keygrips for your key&lt;/span&gt;
gpg &lt;span class=&quot;nt&quot;&gt;--list-keys&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--with-keygrip&lt;/span&gt; YOUR_MASTER_KEY_ID

&lt;span class=&quot;c&quot;&gt;# Look for the keygrip on the line after your [A] (authentication) subkey&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# and add it to sshcontrol, for zsh, bash, fish:&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;YOUR_AUTH_KEYGRIP&quot;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.gnupg/sshcontrol

&lt;span class=&quot;c&quot;&gt;# Or for PowerShell on Windows, but see caveat about gpg-bridge above as this is not mature yet:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# Add-Content -Path &quot;$env:APPDATA\gnupg\sshcontrol&quot; -Value &quot;YOUR_AUTH_KEYGRIP&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Restart the GPG agent for changes to take effect:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;gpgconf &lt;span class=&quot;nt&quot;&gt;--kill&lt;/span&gt; gpg-agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;verifying-it-works&quot;&gt;Verifying It Works&lt;/h3&gt;

&lt;p&gt;With your YubiKey plugged in, you should now see your GPG-backed SSH key:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh-add &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This outputs a public key in SSH format that you can add to GitHub
(&lt;strong&gt;Settings &amp;gt; SSH and GPG keys &amp;gt; New SSH key&lt;/strong&gt;),
paste into a server’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh/authorized_keys&lt;/code&gt;,
or use anywhere else you’d use an SSH key.&lt;/p&gt;

&lt;p&gt;The difference is that when you actually authenticate,
the private key operation happens on the YubiKey—you’ll
see the PIN prompt (or touch, if you enabled it)
just like with commit signing.&lt;/p&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;This setup has served me well for years.
The day-to-day experience is simple: plug in the YubiKey, commit code, enter the PIN when prompted.
The security benefit is significant—your
signing key never persists as a file that could be stolen or accidentally leaked.&lt;/p&gt;

&lt;p&gt;The initial setup is admittedly a bit involved,
especially the key generation and subkey-to-card transfer steps,
but it’s a one-time cost that pays dividends in the form of verified commits and peace of mind
(and dopamine hits from green “Verified” commit labels in GitHub—gets me every time).&lt;/p&gt;

&lt;!-- markdownlint-disable-next-line MD022 --&gt;
&lt;h2 class=&quot;no_toc&quot; id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:pro-git-book&quot;&gt;
      &lt;p&gt;I highly recommend
&lt;a href=&quot;https://git-scm.com/book/en/v2&quot;&gt;Pro Git&lt;/a&gt; by Scott Chacon and Ben Straub.
It’s free to read online,
and it’s the most thorough resource on Git internals and workflows
I’ve come across.
If you want to understand how Git actually works under the hood
rather than just memorizing commands, this is the book. &lt;a href=&quot;#fnref:pro-git-book&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:other-hardware-keys&quot;&gt;
      &lt;p&gt;I mention YubiKey explicitly here
because that’s what I use and it seems to be the most popular,
but other brands of hardware security keys can be used,
like NitroKey, OnlyKey, Token2, and Feitean.
Notably, while Google’s Titan security key supports FIDO2/WebAuthn,
Titan keys do &lt;em&gt;not&lt;/em&gt; support GPG commit signing.
Many of the steps outlined here for YubiKey will be similar for other keys. &lt;a href=&quot;#fnref:other-hardware-keys&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:unaddressed-scenarios&quot;&gt;
      &lt;p&gt;For the diligent and eagle-eyed readers out there,
if you think of a scenario that I haven’t addressed and would like me to add it,
feel free to message me on LinkedIn. &lt;a href=&quot;#fnref:unaddressed-scenarios&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:commit-impersonation&quot;&gt;
      &lt;p&gt;It can happen, and
&lt;a href=&quot;https://www.hanselman.com/blog/how-to-setup-signed-git-commits-with-a-yubikey-neo-and-gpg-and-keybase-on-windows#:~:text=I%20just%20want%20to%20be%20able%20to%20sign%20my%20code%20commits%20to%20GitHub%20so%20I%20might%20avoid%20people%20impersonating%20my%20Git%20Commits%20(happens%20more%20than%20you%27d%20think%20and%20has%20happened%20recently.)&quot;&gt;has happened to some well-known folks out there&lt;/a&gt;.
Also credit to Scott Hanselman whose aforementioned linked post originally got me into all this;
it’s a great guide for Windows.
In his case he uses a key he created from &lt;a href=&quot;https://keybase.io/&quot;&gt;Keybase&lt;/a&gt;,
which is an awesome service that I’ve also used,
but has been a bit dormant since Zoom acquired them. &lt;a href=&quot;#fnref:commit-impersonation&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:gpg-hash-then-sign&quot;&gt;
      &lt;p&gt;Technically, GPG signing uses a hash of the commit content,
which is then signed with your private key.
The verifier re-hashes the commit and checks the signature against your public key. &lt;a href=&quot;#fnref:gpg-hash-then-sign&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:touch-to-sign&quot;&gt;
      &lt;p&gt;You can optionally
&lt;a href=&quot;https://docs.yubico.com/software/yubikey/tools/ykman/OpenPGP_Commands.html#ykman-openpgp-keys-set-touch-options-key-policy&quot;&gt;enable touch-to-sign&lt;/a&gt;
via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman openpgp keys set-touch sig on&lt;/code&gt;
for an extra layer of physical confirmation for &lt;em&gt;each and every signing operation&lt;/em&gt;,
but it’s off by default.
I don’t personally use this because I consider the initial PIN unlock
to be good enough security for my circumstances,
and further when I have coding agents working on things on my machine
I want them to be able to do signed commits without constant intervention. &lt;a href=&quot;#fnref:touch-to-sign&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt; &lt;a href=&quot;#fnref:touch-to-sign:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;sup&gt;2&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:ssh-signing&quot;&gt;
      &lt;p&gt;And given you know how to get SSH going with GitHub,
I assume you can figure out how to set up commit signing with that as well,
and further can figure out how to use an SSH key on a YubiKey—just
ask a couple of questions to your favorite AI and you’ll have that done. &lt;a href=&quot;#fnref:ssh-signing&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:other-distros&quot;&gt;
      &lt;p&gt;I’ll assume that if you’re well-versed in Linux
that you can figure this out for your distro of choice.
While Ubuntu is the most popular,
and hence why I focus on it in this post and others,
I myself tend to lean more towards Fedora these days. &lt;a href=&quot;#fnref:other-distros&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:gpg-email-matching&quot;&gt;
      &lt;p&gt;GitHub matches the commit’s author email
against the UIDs on your GPG key
to decide whether to show the “Verified” badge.
If you commit with multiple email addresses
(e.g. personal and work), you can add additional UIDs to the same key
with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpg --edit-key YOUR_MASTER_KEY_ID&lt;/code&gt; then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adduid&lt;/code&gt;. &lt;a href=&quot;#fnref:gpg-email-matching&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:yubikey-accessories&quot;&gt;
      &lt;p&gt;A couple of practical accessories I use:
I keep a pair of
&lt;a href=&quot;https://www.amazon.com/dp/B0CGLM6PYN&quot;&gt;magnetic USB-C adapters&lt;/a&gt;
on each of my machines so I can pull the YubiKey off MagSafe-style
when moving between them—this
also alleviates the fear of snapping a YubiKey off in a USB port.
And since YubiKeys are small and easy to misplace,
I attached an AirTag on a key ring
(I like the Apple Finewoven AirTag key rings)
along with a small &lt;a href=&quot;https://www.amazon.com/dp/B08RX4V4CM&quot;&gt;hand strap&lt;/a&gt; to make it easier to keep track of. &lt;a href=&quot;#fnref:yubikey-accessories&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:windows-cmd&quot;&gt;
      &lt;p&gt;On Windows, all commands in this post work in PowerShell and Git Bash.
If you use cmd, you’ll need to substitute &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%USERPROFILE%&lt;/code&gt; for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~&lt;/code&gt;
and use backslashes for paths—but
I’d recommend using PowerShell or Git Bash instead. &lt;a href=&quot;#fnref:windows-cmd&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:rsa-still-fine&quot;&gt;
      &lt;p&gt;My own keys are RSA 4096 from 2018, which is still perfectly secure,
but if I were starting fresh today I’d go with ed25519.
If you’re wondering about quantum computing cracking this in the future:
neither RSA nor ed25519 (nor any ECC) is post-quantum encryption—Shor’s
algorithm could theoretically break both.
(I say theoretically because I believe there are
&lt;a href=&quot;https://eprint.iacr.org/2025/1237.pdf&quot;&gt;reasons&lt;/a&gt; to be
&lt;a href=&quot;https://www.schneier.com/blog/archives/2025/07/cheating-on-quantum-computing-benchmarks.html&quot;&gt;skeptical&lt;/a&gt;
about how far along we really are with quantum computing.)
When post-quantum algorithms land in GnuPG, everyone needs to re-key regardless.
In the meantime, the “harvest now, decrypt later” concern
applies primarily to &lt;em&gt;encrypted&lt;/em&gt; data, not &lt;em&gt;signatures&lt;/em&gt;.
For commit signing, the threat from a recovered private key
is forward-looking (forging future signatures as you),
not retroactive—your
past signed commits are already baked into Git’s hash chain
and can’t be altered in-place regardless of what happens to the key. &lt;a href=&quot;#fnref:rsa-still-fine&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:subkey-separation&quot;&gt;
      &lt;p&gt;This is recommended for the same reason you don’t use your root CA
to sign leaf certs—if a subkey is compromised, you revoke just that subkey
without losing your entire identity. &lt;a href=&quot;#fnref:subkey-separation&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:fido2-passkey-pin&quot;&gt;
      &lt;p&gt;The FIDO2 PIN on a YubiKey has its own defaults,
minimum lengths, and retry counter,
managed separately:
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman fido access change-pin&lt;/code&gt; to change the FIDO2 PIN,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman fido access set-pin&lt;/code&gt; if no PIN is set yet,
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ykman fido info&lt;/code&gt; to see the current state. &lt;a href=&quot;#fnref:fido2-passkey-pin&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:fido2-passkeys&quot;&gt;
      &lt;p&gt;FIDO2 and WebAuthn are the underlying standards behind
what most people now know as
&lt;a href=&quot;https://fidoalliance.org/passkeys/&quot;&gt;passkeys&lt;/a&gt;.
A YubiKey can act as a hardware-bound passkey for logging into services
like GitHub, Google, and Microsoft—separate
from GPG signing, but using the same physical device. &lt;a href=&quot;#fnref:fido2-passkeys&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:stolen-yubikey-risk&quot;&gt;
      &lt;p&gt;You might wonder whether I rotated my signing key
after the YubiKey was stolen.
I didn’t—the private key cannot be extracted from a YubiKey,
and the PIN retry counter locks the card after 3 failed attempts,
so the practical risk was near zero.
If you want to be extra cautious in a similar situation,
you should revoke the subkey and generate a new one
from your backed-up master key. &lt;a href=&quot;#fnref:stolen-yubikey-risk&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:keys-openpgp-org&quot;&gt;
      &lt;p&gt;I recommend &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keys.openpgp.org&lt;/code&gt; specifically
because it requires email verification before publishing your identity.
When you upload a key, the server strips all UIDs (email addresses)
and sends a verification email to each address on the key.
Only after you click the confirmation link
does the server associate that email with your key
and make it searchable by email address.
This means no one can upload a key claiming to be you
without access to your inbox.
Traditional SKS keyservers (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgp.mit.edu&lt;/code&gt;) have no such verification,
which led to issues like the 2019 certificate flooding attack. &lt;a href=&quot;#fnref:keys-openpgp-org&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sat, 11 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/04/a-no-nonsense-guide-to-gpg-commit-signing-with-a-yubikey/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/04/a-no-nonsense-guide-to-gpg-commit-signing-with-a-yubikey/</guid>
        
        <category>gpg</category>
        
        <category>yubikey</category>
        
        <category>security</category>
        
        <category>git</category>
        
        
      </item>
    
      <item>
        <title>The Time I Accidentally Dropped a Database</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>A story about accidentally dropping a shared dev database, asking a DBA to revoke my own permissions, and why the principle of least privilege matters more than ever in the age of AI agents.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/Pieter_Bruegel_the_Elder_-_Landscape_with_the_Fall_of_Icarus_-_Brussels,_Royal_Museums_of_Fine_Arts_of_Belgium_-_Google_Arts_&amp;amp;_Culture.jpg&quot; alt=&quot;A pastoral landscape with a farmer plowing in the foreground, a shepherd gazing upward, and ships in a harbor, while in the lower right corner a pair of legs disappears into the sea — the fall of Icarus, unnoticed by all.&quot; /&gt;
&lt;em&gt;“Landscape with the Fall of Icarus,” after Pieter Bruegel the Elder, c. 1560s.
Royal Museums of Fine Arts of Belgium.
Via &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Pieter_Bruegel_the_Elder_-_Landscape_with_the_Fall_of_Icarus_-_Brussels,_Royal_Museums_of_Fine_Arts_of_Belgium_-_Google_Arts_%26_Culture.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over a decade ago,
I was working on a system
that allowed users to work with images
of exterior paint test panels from exposure stations
at what we called
“the &lt;a href=&quot;https://www.pcimag.com/articles/102269-a-look-at-dows-paint-farm-and-technology-center&quot;&gt;paint farm&lt;/a&gt;.”
The backend that helped to track all of this
was an ASP.NET Web API with Entity Framework and SQL Server,
and I was working on that part of the stack,
iterating on the database schema
and its OData-based API.
(Sidebar: &lt;a href=&quot;https://www.odata.org/&quot;&gt;OData&lt;/a&gt; is awesome,
an actual standard for REST
which should have had much more adoption than it did.
Sigh.
But I digress…)&lt;/p&gt;

&lt;p&gt;If you’ve worked with Entity Framework code-first migrations,
you know the drill.
You’re iterating on your data model in C# model classes,
running migrations,
and occasionally it’s helpful to just blow away your LocalDB
and let EF recreate it from scratch.
Clean slate,
fresh seed data
(where it’s usually helpful to add one or more records
to each table for testing),
move on with your day.&lt;/p&gt;

&lt;p&gt;The thing about SQL Server Management Studio is that it’s very easy
to be connected to more than one server at a time.
LocalDB sits right there in your Object Explorer,
and so does your shared dev instance,
the one that a large handful of other developers rely on
for testing their own applications against your API.&lt;/p&gt;

&lt;p&gt;I can’t recall every detail of exactly how it happened.
But the part that lodged itself into my amygdala—where
it remains to this day—was
the moment I realized I had just dropped the dev database.&lt;/p&gt;

&lt;p&gt;Not &lt;em&gt;my&lt;/em&gt; LocalDB.
The &lt;em&gt;shared&lt;/em&gt; dev database.&lt;/p&gt;

&lt;h2 id=&quot;the-aftermath&quot;&gt;The Aftermath&lt;/h2&gt;

&lt;p&gt;I noticed immediately.
That particular flavor of dread is unmistakable,
the kind where your stomach drops faster than the database did.&lt;/p&gt;

&lt;p&gt;I went hat in hand to our DBA,
explained what happened,
and asked for a restore.
Fortunately,
he had hourly incremental backups running,
and we were able to get the database back up quickly.
Nobody gave me a hard time about it.&lt;/p&gt;

&lt;p&gt;Nobody except myself.&lt;/p&gt;

&lt;h2 id=&quot;the-corrective-action&quot;&gt;The Corrective Action&lt;/h2&gt;

&lt;p&gt;Here’s the part of the story that surprised even the DBA.&lt;/p&gt;

&lt;p&gt;I went back to him and said:
“Can you revoke my permission to drop databases on the dev server?”&lt;/p&gt;

&lt;p&gt;He was taken aback.
Even for a dev environment,
voluntarily asking for &lt;em&gt;fewer&lt;/em&gt; permissions isn’t something
people typically do.&lt;/p&gt;

&lt;p&gt;But I thought about it,
and the answer was obvious:
I literally &lt;em&gt;never&lt;/em&gt; needed to drop that database.
Not once.
Not ever.
I could still connect,
run queries,
migrate schemas,
do everything my actual work required.
The ability to drop the database was a permission I held
but had zero legitimate use for.
So why keep it?&lt;/p&gt;

&lt;p&gt;This was very much an SRE mindset.
When an incident happens,
even a low-stakes one,
you take corrective action so it doesn’t happen again.
You don’t just say “I’ll be more careful next time.”
You change the system.&lt;/p&gt;

&lt;h2 id=&quot;it-happened-again-sort-of&quot;&gt;It Happened Again (Sort Of)&lt;/h2&gt;

&lt;p&gt;Fast forward to more recent times.
My team and I had stood up an Azure OpenAI sandbox
that many folks across the organization were using day-to-day
for proofs of concept.&lt;/p&gt;

&lt;p&gt;The Azure Portal has an undesirable UI paradigm
where the “Delete resource group” button lives dangerously close to
the “Delete resource” button.
You can probably see where this is going.&lt;/p&gt;

&lt;p&gt;I clicked the wrong one at the wrong screen.
And immediately realized what I’d done.&lt;/p&gt;

&lt;p&gt;(You may be asking why we weren’t using IaC—well,
we were, but in those early days of Azure OpenAI
things could get quite finnicky,
and sometimes we’d need to intervene manually,
and while IaC is helpful for &lt;em&gt;standing up&lt;/em&gt; infrastructure,
tearing it down doesn’t get exercised quite as often.)&lt;/p&gt;

&lt;p&gt;Luckily,
we were able to recreate the resource group
and restore the exact same resources inside it with some scripting
(and there’s nothing like scripting under pressure)
before it caused too much disruption.
But the outcome was the same as before:
we didn’t just move on and hope it wouldn’t happen again.
We introduced
&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/lock-resources?tabs=json&quot;&gt;resource locks&lt;/a&gt;
on the resource group and resources in our Azure OpenAI sandbox
to prevent accidental deletion going forward
(because, really, how often do you ever need to be able to delete a whole resource group,
even a non-prod one;
and even when you do,
you can take a moment to remove the locks).&lt;/p&gt;

&lt;h2 id=&quot;so-what-does-this-have-to-do-with-ai&quot;&gt;So what does this have to do with AI?&lt;/h2&gt;

&lt;p&gt;I keep thinking about these experiences
now that we’re handing AI agents the keys to real systems.&lt;/p&gt;

&lt;p&gt;The principle of least privilege isn’t new.
Since I first started working with AWS,
we’ve always stressed crafting IAM roles
with only the permissions a service actually needs.
But there’s something worth stating plainly:
if there’s a permission you don’t ever want
an AI agent to use,
then simply don’t grant it.&lt;/p&gt;

&lt;p&gt;That’s it.
Same lesson I learned the hard way in 2014.&lt;/p&gt;

&lt;p&gt;What’s different now is that AI is actually good at the tedious part:
writing tightly scoped IAM policies in infrastructure as code,
the kind of work people skip because it’s boring.
And it’s going to matter a lot more
when the services behind those roles
are themselves AI agents.&lt;/p&gt;

&lt;p&gt;The industry is still in the throes of discussing sandboxing for AI,
but even the most sandboxed agent,
when given a permission it doesn’t need,
in the absence of additional checks on commands it would run,
still has the potential to do bad things,
like drop a database…
So the principle is the same one
I stumbled into when I, as a human, dropped a dev database:
don’t give yourself, or your agents,
permissions you don’t really need.&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 04 Apr 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/04/the-time-i-accidentally-dropped-a-database/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/04/the-time-i-accidentally-dropped-a-database/</guid>
        
        <category>ai</category>
        
        <category>security</category>
        
        <category>sre</category>
        
        
      </item>
    
      <item>
        <title>Jevons Paradox and the Future of Software Engineering</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>When things get cheaper, we use more of them, not less. William Stanley Jevons figured this out about coal in 1865. The cloud proved him right. AI will too, and that&apos;s good news for software engineers.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/William_Stanley_Jevons_portrait_extract.jpg&quot; alt=&quot;Portrait of William Stanley Jevons.&quot; /&gt;
&lt;em&gt;&lt;a href=&quot;https://commons.wikimedia.org/wiki/File:William_Stanley_Jevons_portrait_extract.jpg&quot;&gt;William Stanley Jevons (via University of Manchester Libraries)&lt;/a&gt;, &lt;a href=&quot;https://creativecommons.org/licenses/by-sa/4.0&quot;&gt;CC BY-SA 4.0&lt;/a&gt;, via Wikimedia Commons.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here’s something that shouldn’t be true but is:
when things get cheaper, we often spend more on them.&lt;/p&gt;

&lt;p&gt;Think about lemons. 🍋&lt;/p&gt;

&lt;p&gt;In the 1800s,
a lemon was an exotic luxury,
a tropical fruit shipped from far away at great expense.
Only the wealthy could afford them regularly.
As shipping and agriculture improved,
the price per lemon plummeted.&lt;/p&gt;

&lt;p&gt;And did we buy fewer lemons? Did we spend less on them?&lt;/p&gt;

&lt;p&gt;Of course not.
We put them in everything.
Lemonade, lemon meringue pie, lemon zest on fish,
lemon in our water, lemon-scented cleaning products.
The cheaper lemons got,
the more uses we found for them,
and the more we spent on them in aggregate.&lt;/p&gt;

&lt;p&gt;This is Jevons Paradox,
and it’s one of those ideas in economics
that sounds wrong until you see it everywhere.
Most people who invoke it do so in passing,
a quick reference to sound clever in a meeting.
But the man behind it and his original argument
deserve a deeper look,
because the pattern he identified in 1865
is playing out right now with AI
and software engineering.&lt;/p&gt;

&lt;h2 id=&quot;the-man-behind-the-paradox&quot;&gt;The Man Behind the Paradox&lt;/h2&gt;

&lt;p&gt;William Stanley Jevons was born in Liverpool in 1835,
the ninth of eleven children
in a family of iron merchants and hardware manufacturers.
He was,
by any measure,
a polymath.&lt;/p&gt;

&lt;p&gt;Jevons studied chemistry and mathematics at University College London,
then spent five years in Sydney, Australia
as an assayer at the Royal Mint,
testing the purity of gold during the Australian gold rush.
He returned to England
and became one of the founders
of the &lt;a href=&quot;https://en.wikipedia.org/wiki/Marginalism&quot;&gt;marginalist revolution&lt;/a&gt; in economics,
fundamentally changing how economists think about value.
He also built a mechanical reasoning machine
called the
&lt;a href=&quot;https://old.maa.org/press/periodicals/convergence/mathematical-treasure-jevons-pure-logic-logic-piano&quot;&gt;“logic piano”&lt;/a&gt;,
essentially an early mechanical computer,
decades before anything resembling modern computing.&lt;/p&gt;

&lt;p&gt;Jevons died in 1882 at the age of 46,
drowning while swimming near Hastings.
He left behind a body of work
that economists and logicians still reference today.&lt;/p&gt;

&lt;p&gt;But the thing most people know him for,
if they know him at all,
is a book he published in 1865 at the age of 29.&lt;/p&gt;

&lt;h2 id=&quot;the-coal-question&quot;&gt;The Coal Question&lt;/h2&gt;

&lt;p&gt;In 1865,
Jevons published
&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Coal_Question&quot;&gt;&lt;em&gt;The Coal Question: An Inquiry Concerning the Progress of the Nation, and the Probable Exhaustion of Our Coal Mines&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The prevailing wisdom at the time
was reassuring:
James Watt’s far more efficient steam engine
(yes, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Watt&quot;&gt;watt&lt;/a&gt; is named after him)
would &lt;em&gt;reduce&lt;/em&gt; Britain’s coal consumption.
After all,
if each engine uses less coal per unit of work,
the nation should need less coal overall.
Simple math, right?&lt;/p&gt;

&lt;p&gt;Jevons saw it differently.&lt;/p&gt;

&lt;p&gt;He argued that Watt’s improvements
made coal-powered industry so much more economical
that it opened up entirely new applications.
Before the efficient steam engine,
coal powered a narrow set of uses,
mostly pumping water out of mines.
After Watt,
coal powered factories, locomotives, steamships, and heating.
The efficiency gains didn’t reduce demand,
they &lt;em&gt;exploded&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;As Jevons wrote in his book:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“It is wholly a confusion of ideas to suppose
that the economical use of fuel is equivalent
to a diminished consumption.
The very contrary is the truth.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the paradox:
technological improvements
that increase the efficiency of resource use
tend to &lt;em&gt;increase&lt;/em&gt; the total consumption of that resource,
not decrease it.
The more efficiently you can use something,
the more uses you find for it,
and the more of it you consume.&lt;/p&gt;

&lt;p&gt;Watt’s steam engine wasn’t a conservation device.
It was a device that &lt;em&gt;unlocked more value&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-cloud-proved-him-right&quot;&gt;The Cloud Proved Him Right&lt;/h2&gt;

&lt;p&gt;I’ve watched this paradox play out firsthand
over a decade of working with cloud computing.&lt;/p&gt;

&lt;p&gt;Earlier in my career,
I had a manager who would side-eye me
about the cost of a single thickly provisioned API gateway.
It was a meaningful line item,
the kind of thing that showed up in budget reviews
and required justification.&lt;/p&gt;

&lt;p&gt;A few years later,
that same offering became serverless:
consumption-based, pay-per-call, scaling to actual demand.
The cost per unit fell so far
that the side-eye went away.&lt;/p&gt;

&lt;p&gt;And what could I do with that newfound freedom?&lt;/p&gt;

&lt;p&gt;I wouldn’t run one API gateway more cheaply.
I would run &lt;em&gt;a hundred of them&lt;/em&gt; for different use cases,
produce far more value,
spend roughly &lt;em&gt;the same amount of money&lt;/em&gt;,
and nobody would bat an eye.&lt;/p&gt;

&lt;p&gt;This is Jevons Paradox in the real world.&lt;/p&gt;

&lt;p&gt;The broader cloud story follows the same pattern.
Year after year,
the cost per compute unit has dropped.
Serverless architectures and managed services emerged
that scale to demand
(you only pay for what you use).
Spot instances, reserved capacity, autoscaling.
All of these drove the unit economics down.&lt;/p&gt;

&lt;p&gt;And yet,
cloud bills across the industry went &lt;em&gt;up&lt;/em&gt;,
not down.&lt;/p&gt;

&lt;p&gt;Not because companies were being wasteful
(although some were),
but because cheaper compute unlocked use cases
that weren’t viable before.
Workloads that couldn’t justify the infrastructure cost
at $X per hour
suddenly made perfect sense at $X/10 per hour.
So companies ran more workloads.
And more.
And more.&lt;/p&gt;

&lt;p&gt;The cloud got cheaper.
We used more of it.
Jevons was right.&lt;/p&gt;

&lt;h2 id=&quot;ai-is-the-next-steam-engine&quot;&gt;AI Is the Next Steam Engine&lt;/h2&gt;

&lt;p&gt;I see the same pattern emerging with AI
and software engineering,
and I want to take a clear stance here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI will increase the demand for software engineers,
not decrease it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I know that’s not the dominant narrative right now.
The headlines are full of
&lt;a href=&quot;https://www.theguardian.com/technology/2026/mar/03/jack-dorsey-block-ai-worker-jobs&quot;&gt;layoffs and hiring slowdowns&lt;/a&gt;,
with companies citing AI-driven efficiency as the reason.
But look closer at these stories
and the picture gets murkier.
It’s hard to separate
“AI made us more efficient”
from
“we needed to cut costs anyway and AI is a convenient narrative.”&lt;/p&gt;

&lt;p&gt;Correlation is not causation,
and a convenient story is not evidence.&lt;/p&gt;

&lt;p&gt;There will be an initial period
(we may be in it now)
where
some companies look at AI-assisted engineering
and see an opportunity to do the same work with fewer people.
And some of those companies will be right,
for a time.&lt;/p&gt;

&lt;p&gt;But businesses are &lt;em&gt;always&lt;/em&gt; insatiable.
They always have a backlog that stretches to the horizon,
features they can’t build,
markets they can’t enter,
technical debt they can’t address,
experiments they can’t run.
The bottleneck has always been
the cost and availability of engineering talent.&lt;/p&gt;

&lt;p&gt;When AI makes each engineer significantly more productive,
when it turns a task that took a week into one that takes a day,
businesses
won’t say
“great, we can cut 80% of our engineers.”
They’ll say
“great, now we can finally build that thing
we’ve been putting off for three years.”&lt;/p&gt;

&lt;p&gt;Just like Watt’s steam engine
didn’t make Britain say
“wonderful, we need less coal.”
It made Britain say
“what &lt;em&gt;else&lt;/em&gt; can we power with this?”&lt;/p&gt;

&lt;p&gt;(I’ve also seen AI compared to electricity and the printing press.
Pick your poison / analogy.)&lt;/p&gt;

&lt;h2 id=&quot;ive-seen-this-in-my-own-work&quot;&gt;I’ve Seen This in My Own Work&lt;/h2&gt;

&lt;p&gt;I wrote recently about
&lt;a href=&quot;/2026/02/a-tale-of-acceleration-and-compound-engineering/&quot;&gt;acceleration and compound engineering&lt;/a&gt;,
how
a dev machine setup practice I’ve carried across jobs for years
took me &lt;em&gt;months&lt;/em&gt; to modernize with GitHub Copilot last summer,
but this year,
Claude Code wrapped it in CI in ninety minutes
and added a new Linux distro in under an hour.&lt;/p&gt;

&lt;p&gt;Here’s the thing:
I didn’t take those efficiency gains and go sit on a beach
(although that sounds really nice).
I immediately turned around and did &lt;em&gt;more&lt;/em&gt;.
More distros. More CI. More polish.
Things that weren’t worth the effort before
became trivially achievable,
so I did them.&lt;/p&gt;

&lt;p&gt;My backlog didn’t get smaller.
It got &lt;em&gt;different&lt;/em&gt;.
Projects that I’d mentally filed under
“someday when I have a free month”
became “I can knock that out this afternoon.”
And when those were done,
I found new things to build that I hadn’t even considered before,
because the cost of attempting them had dropped below the threshold
where they were worth thinking about.&lt;/p&gt;

&lt;p&gt;This is exactly what Jevons described.
The efficiency of the steam engine
didn’t reduce coal consumption.
It unlocked new applications
that nobody had previously considered viable.&lt;/p&gt;

&lt;h2 id=&quot;the-counterarguments&quot;&gt;The Counterarguments&lt;/h2&gt;

&lt;p&gt;It’s worth noting
that Jevons Paradox doesn’t &lt;em&gt;always&lt;/em&gt; hold.
Economists have identified cases
where efficiency improvements really do reduce total consumption.&lt;/p&gt;

&lt;p&gt;The key variable is &lt;em&gt;elasticity of demand&lt;/em&gt;.
If demand for a resource is relatively inelastic,
if people don’t actually want much more of it
even when it’s cheap,
then
efficiency gains can reduce total consumption.
LED lightbulbs appear to have actually
&lt;a href=&quot;https://www.iea.org/commentaries/the-next-wave-of-led-lighting-smarter-circular-and-more-efficient&quot;&gt;reduced total electricity used for lighting&lt;/a&gt;,
because there’s only so much light people want in their homes.&lt;/p&gt;

&lt;p&gt;But zoom out from lighting
and look at electricity as a whole.
Demand for power is surging,
driven largely by data centers
that didn’t exist at this scale a decade ago.
LEDs saved watts in the living room;
AI is consuming megawatts in the server farm.
Same resource, opposite outcomes,
depending on where the demand is elastic.&lt;/p&gt;

&lt;p&gt;And software?
Software demand is about as elastic as it gets.
Every business wants more of it.
Every industry is being reshaped by it.
The backlog of software that the world wants built
is effectively infinite.&lt;/p&gt;

&lt;p&gt;When the constraint is demand,
efficiency gains can reduce consumption.
When the constraint is &lt;em&gt;supply&lt;/em&gt;,
efficiency gains
&lt;a href=&quot;https://www.youtube.com/watch?v=7_PX1cVuaVA&quot;&gt;blow the doors off&lt;/a&gt;.
Software engineering is firmly in the latter camp.&lt;/p&gt;

&lt;h2 id=&quot;token-budgets-are-the-new-cloud-bills&quot;&gt;Token Budgets Are the New Cloud Bills&lt;/h2&gt;

&lt;p&gt;Here’s my prediction for how this plays out:&lt;/p&gt;

&lt;p&gt;The engineers who learn to wield AI effectively,
who build the compound practices
(testing, CI, structured prompting, agentic workflows)
and discover entirely new ways of delivering software,
will be the ones everyone wants to hire.
There will be &lt;em&gt;more to build&lt;/em&gt; than ever before.&lt;/p&gt;

&lt;p&gt;And businesses won’t spend less on software engineering.
They’ll spend &lt;em&gt;differently&lt;/em&gt;.
Where today the cost is primarily headcount,
a growing share is already token budgets,
the cost of the AI compute
that amplifies each engineer’s output.
We saw the same shift in cloud computing:
the engineers who learned to automate infrastructure
were the ones everyone wanted.&lt;/p&gt;

&lt;p&gt;Just like cloud bills replaced data center CapEx
and then &lt;a href=&quot;https://www.cio.com/article/247214/cloud-computing-has-its-jevons-moment.html&quot;&gt;grew beyond&lt;/a&gt;
what &lt;a href=&quot;https://x.com/swardley/status/1884305635476701269&quot;&gt;&lt;em&gt;most&lt;/em&gt; people expected&lt;/a&gt;,
token budgets will become a major line item
that grows year over year.
As the cost per token comes down,
our consumption will go up.&lt;/p&gt;

&lt;p&gt;William Stanley Jevons figured this out
about coal over 160 years ago.
The cloud proved him right.
AI will prove him right again.&lt;/p&gt;

&lt;p&gt;The paradox endures:
when something gets cheaper,
we don’t use less.
We use more.&lt;/p&gt;

&lt;p&gt;This is not a threat to software engineers,
but there’s an uncomfortable flip side.
If the cost per token keeps dropping
and engineers around you are multiplying their output with these tools,
then choosing not to use them
makes &lt;em&gt;you&lt;/em&gt; the expensive resource per unit of output.
You don’t want to be the one still rationing coal
while everyone else is building modern steam engines.
We have the greatest of power tools now,
and it’s up to us to use them.&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/03/jevons-paradox-and-the-future-of-software-engineering/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/03/jevons-paradox-and-the-future-of-software-engineering/</guid>
        
        <category>ai</category>
        
        <category>economics</category>
        
        <category>software-engineering</category>
        
        
      </item>
    
      <item>
        <title>Curating My Own Algorithm</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>A follow-up to my previous post on information diets. This time I get specific about the tools, feeds, and habits that make up my personal approach to cutting through the noise and the slop.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/Édouard_Manet_-_Woman_Reading_-_1933.435_-_Art_Institute_of_Chicago.jpg&quot; alt=&quot;An impressionist oil painting of a woman in a black hat and dark dress reading a newspaper in a sunlit garden.&quot; /&gt;
&lt;em&gt;“Woman Reading” — Édouard Manet, public domain,
via &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:%C3%89douard_Manet_-_Woman_Reading_-_1933.435_-_Art_Institute_of_Chicago.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a previous post,
&lt;a href=&quot;/2025/07/your-information-diet-in-the-age-of-ai/&quot;&gt;Your Information Diet in the Age of AI&lt;/a&gt;,
I talked broadly about why it matters to be intentional with what you read, watch, and listen to.
This time I want to get specific.
Here is what my information diet actually looks like in practice,
and how I use it to cut through the noise and the slop.&lt;/p&gt;

&lt;h2 id=&quot;leaving-most-social-media-behind&quot;&gt;Leaving (Most) Social Media Behind&lt;/h2&gt;

&lt;p&gt;I spent many years on Twitter.
(To give you a sense of how long that was,
I was there in the very early days
when the way you tweeted was by sending an SMS text with your tweet to the phone number 40404.)
When I finally left,
I realized something:
I had given a ton of my time, attention, energy, and thoughts to that platform
when I could have been making and owning my own content—like writing blog posts.&lt;/p&gt;

&lt;p&gt;That stuck with me.
Today I only have a handful of social media accounts:
&lt;a href=&quot;https://www.linkedin.com/in/ryanspletzer/&quot;&gt;LinkedIn&lt;/a&gt;,
&lt;a href=&quot;https://bsky.app/&quot;&gt;Bluesky&lt;/a&gt;,
and &lt;a href=&quot;https://hachyderm.io/&quot;&gt;Mastodon&lt;/a&gt;.
I only post to them when I share something I’ve written here.
For the little social media I do have,
I don’t have any of those apps on my phone.
That one change alone has done more for my mental health and focus than I expected,
and I find myself with more time and energy for programming and writing
and the finer things in life.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-with-algorithms&quot;&gt;The Problem with Algorithms&lt;/h2&gt;

&lt;p&gt;Algorithms in social media are a mixed bag.
Some are so good at figuring out what you want that they become addicting.
TikTok’s algorithm, for example, is eerily good at this.
It has a level of randomness that takes you into interesting adjacent content
you never would have sought out yourself.
I don’t use it anymore for exactly that reason:
it’s &lt;em&gt;too&lt;/em&gt; good at keeping you glued to the screen.&lt;/p&gt;

&lt;p&gt;Then there are algorithms that are just bad.
YouTube, which I otherwise love as a platform,
has a recommendation engine with a tunnel vision problem.
Watch one cat video and it assumes the next ten things you want to see are more cat videos.
It lacks the serendipity that makes discovery interesting.
LinkedIn’s feed has its own issues—you
still see good things from people you respect,
but let’s be honest: those good things are floating around in a sea of slop.&lt;/p&gt;

&lt;p&gt;Bluesky and Mastodon don’t feel nearly as algorithm-driven,
which is refreshing.&lt;/p&gt;

&lt;h2 id=&quot;rss-my-personal-feed&quot;&gt;RSS: My Personal Feed&lt;/h2&gt;

&lt;p&gt;My solution to a lot of this is &lt;a href=&quot;https://en.wikipedia.org/wiki/RSS&quot;&gt;RSS&lt;/a&gt;,
a simple open protocol that’s been around since the late ’90s.
It lets websites publish a feed of their content,
and you subscribe to whichever feeds you want in a reader app.
No account required, no algorithm deciding what you see,
no company in the middle.
RSS and its sibling protocol &lt;a href=&quot;https://en.wikipedia.org/wiki/Atom_(web_standard)&quot;&gt;Atom&lt;/a&gt;
are also what power podcasts under the hood—every
podcast app is really just a specialized feed reader
that fetches audio and video files instead of articles.&lt;/p&gt;

&lt;p&gt;Now on to my modern RSS feed setup:
I heard about &lt;a href=&quot;https://feedbin.com/&quot;&gt;Feedbin&lt;/a&gt; and
&lt;a href=&quot;https://netnewswire.com/&quot;&gt;NetNewsWire&lt;/a&gt; from
&lt;a href=&quot;https://twit.tv/&quot;&gt;Leo Laporte&lt;/a&gt; on the TWiT network,
and the setup has been great.&lt;/p&gt;

&lt;p&gt;Feedbin is a hosted RSS service with a small subscription fee,
but what you get for it is worth it:
it syncs your read/unread state across any RSS client you connect to it.
That keeps you portable.
You can use NetNewsWire on your Mac,
Flipboard on an iPad,
or try out a completely different reader—your
state follows you everywhere.
If you remember Google Reader, this kind of setup gets you back to a similar experience.
(We all miss Google Reader.)&lt;/p&gt;

&lt;p&gt;I currently subscribe to about 125 feeds
(and counting—this number will be out-of-date quickly),
organized into two categories:
&lt;strong&gt;news&lt;/strong&gt; and &lt;strong&gt;tech&lt;/strong&gt;.
I strive to keep it simple.&lt;/p&gt;

&lt;p&gt;You would think that the sheer number of feeds would be overwhelming,
but honestly besides the official news/tech publishing outfits
(whose headlines you can often gloss over),
the posts that come from individuals
and even many organizations like companies’ engineering blogs
are more of a trickle,
and since they are more well-thought-out blog posts from people,
it is not the same torrent as an incessant feed of quick thoughts from people
coming through social feeds.
The social media companies are &lt;em&gt;always&lt;/em&gt; going to feed you more,
whereas with this RSS approach I feel like I control the firehose.&lt;/p&gt;

&lt;p&gt;My news category is deliberately lean—just a handful of sources.
I started with more, but when you add major news outlets into a
drama-free RSS reader,
you suddenly realize from the headlines alone how much of the content
is either garbage or just there to sensationalize and stoke fear.
The clean, linear presentation of RSS strips away
the layout and design that masks the fear-mongering.
It becomes a litmus test for quality.
I won’t name names on the ones I cut,
but I will give a shout-out to the
&lt;a href=&quot;https://www.bbc.com/news&quot;&gt;BBC&lt;/a&gt;—they
consistently get it right.&lt;/p&gt;

&lt;p&gt;My tech category is where the bulk of my feeds live.
It’s a mix of individual blogs from people I respect,
tech publications, product and leadership voices,
company engineering blogs,
AI-focused sources, and web development resources.
I won’t list them all here—the
whole point is that you should curate your own.
Everyone is at a different point in their journey,
and what I find useful might not be what you need.&lt;/p&gt;

&lt;p&gt;As an interesting example of curation,
I recently made a deliberate decision
to add more product management thought leaders into my mix of feeds.
That’s one of the nice things about this approach—you
can intentionally expand into new areas rather than waiting for an algorithm
to maybe surface something relevant.&lt;/p&gt;

&lt;h2 id=&quot;linkedin-as-discovery-rss-as-delivery&quot;&gt;LinkedIn as Discovery, RSS as Delivery&lt;/h2&gt;

&lt;p&gt;One pattern I’ve settled into:
I appreciate when people post on LinkedIn,
but on LinkedIn you may miss things,
and what you see might only be snippets rather than the full picture.
So when I find someone who consistently writes good stuff,
I go find their actual website and subscribe via RSS.
LinkedIn is how I find people;
RSS is how I actually read them.&lt;/p&gt;

&lt;p&gt;A handy thing worth mentioning:
you can subscribe to people on &lt;a href=&quot;https://medium.com/&quot;&gt;Medium&lt;/a&gt; and
&lt;a href=&quot;https://substack.com/&quot;&gt;Substack&lt;/a&gt; via RSS too.
You don’t have to use their apps or get their emails.
Just grab the RSS feed URL by appending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/feed&lt;/code&gt; to Substack URLs
or inserting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/feed/username&lt;/code&gt; for Medium
and add it to your reader.&lt;/p&gt;

&lt;p&gt;For example,
I recently followed &lt;a href=&quot;https://medium.com/airbnb-engineering&quot;&gt;The Airbnb Tech Blog&lt;/a&gt; on Medium.
To get the feed URL,
I took their URL &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://medium.com/airbnb-engineering&lt;/code&gt;
and inserted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;feed&lt;/code&gt; to get &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://medium.com/feed/airbnb-engineering&lt;/code&gt;.
If the Medium blog uses a vanity domain name,
just appending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/feed&lt;/code&gt; does the trick.
In very rare cases there are “blogs” I want to follow
that come through subreddits—there’s
a &lt;a href=&quot;https://www.howtogeek.com/320264/how-to-get-an-rss-feed-for-any-subreddit/&quot;&gt;trick&lt;/a&gt;
for subscribing to those with RSS as well,
by appending &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.rss&lt;/code&gt; to the subreddit name in the URL.&lt;/p&gt;

&lt;p&gt;(Or, you know, just ask your favorite AI
“What is the RSS feed for XYZ blog”
and it will probably give you the answer.)&lt;/p&gt;

&lt;p&gt;Several of the feeds I follow are Medium or Substack authors
consumed entirely through Feedbin (with NetNewWire atop).&lt;/p&gt;

&lt;p&gt;Now, unlike the good ol’ days,
you may not be able to read the full length of every article in the reader app,
but that isn’t really the end of the world—the
point is to have a feed of high quality content,
and if you have to pop out to the browser to read something,
that is fine;
people need to eat,
and they often need the click-throughs for ad revenue, etc.&lt;/p&gt;

&lt;h2 id=&quot;what-it-feels-like&quot;&gt;What It Feels Like&lt;/h2&gt;

&lt;p&gt;What I’m really doing with all of this is curating my own algorithm.
Admittedly, maybe calling it an “algorithm” is a stretch,
because it’s very simple and linear,
and it’s only filled with things I consider to be high quality.&lt;/p&gt;

&lt;p&gt;My routine is a morning and evening scan.
I can scroll through headlines and be in and out in a couple of minutes.
If there’s a great post from someone I respect, I take the time to read it,
either in the moment or by popping it out into my browser for later.
But by and large,
the whole experience takes far less time than social media
and is far less agitating.&lt;/p&gt;

&lt;p&gt;Compare that to reaching for a social media app—you
open it for “just a minute” and get sucked into a vortex.
With RSS there’s no infinite scroll designed to trap you,
no outrage-bait wedged between the posts you actually care about.
You read what’s there and you’re done.&lt;/p&gt;

&lt;p&gt;It’s healthier.
I feel less anxiety, less fear from sensationalized headlines,
and I have more bandwidth for the things that matter to me—writing,
building things, and thinking clearly.&lt;/p&gt;

&lt;h2 id=&quot;how-you-can-do-this-too&quot;&gt;How You Can Do This Too&lt;/h2&gt;

&lt;p&gt;If any of this resonates, here’s the practical version:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Pick a sync service and a reader.&lt;/strong&gt;
I use &lt;a href=&quot;https://feedbin.com/&quot;&gt;Feedbin&lt;/a&gt; with
&lt;a href=&quot;https://netnewswire.com/&quot;&gt;NetNewsWire&lt;/a&gt;,
but there are plenty of options.
The key is a service that syncs state across devices
so you’re not tied to one app.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Start subscribing.&lt;/strong&gt;
Think about the people whose writing you value—bloggers,
newsletter authors, journalists.
Find their RSS feeds.
Most blogs have one;
Medium and Substack do, too.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Organize into categories&lt;/strong&gt; that make sense to you.
Mine are simple: news and tech.
Yours might be different.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Prune ruthlessly.&lt;/strong&gt;
Add sources liberally at first, then cut anything
that consistently disappoints.
Headlines in a clean reader will tell you a lot
about a source’s real quality.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remove social apps from your phone.&lt;/strong&gt;
This was a big one for me.
You can still use those platforms on a computer when you choose to,
but removing the temptation to reach for them
in idle moments makes a real difference.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Give it time.&lt;/strong&gt;
The payoff isn’t instant,
but after a few weeks you’ll notice you feel better informed
with less effort and less anxiety.
Especially after removing social media apps from the phone,
you’ll find your thumb instinctively reaching
for where those apps used to be in your phone’s app grid—I
found out that this unconscious reflex subsides after a few days to a week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s my information diet.
Fewer social algorithms, no doomscrolling.
Just a feed I built myself, filled with things worth reading.&lt;/p&gt;

&lt;p&gt;And reading is good.
Just ask &lt;a href=&quot;https://www.youtube.com/watch?v=OAIW5se_cxg&quot;&gt;LeVar Burton&lt;/a&gt;. 🌈&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/03/curating-my-own-algorithm/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/03/curating-my-own-algorithm/</guid>
        
        <category>information-diet</category>
        
        <category>rss</category>
        
        
      </item>
    
      <item>
        <title>Shedding Dead Context</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>More plugins, more extensions, more context doesn&apos;t mean better results. It often means worse. Your AI&apos;s context window is finite memory, and most people are wasting it before they&apos;ve typed a real prompt.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/1024px-Edvard_Munch_-_Anxiety_-_Google_Art_Project.jpg&quot; alt=&quot;A crowd of ghostly, pale-faced figures in dark clothing and top hats walk along a pier against a turbulent, swirling sky of deep blues and oranges, their hollow expressions conveying collective dread and unease.&quot; /&gt;
&lt;em&gt;Edvard Munch, Anxiety, 1894. Munch Museum, Oslo. Public domain,
via &lt;a href=&quot;https://commons.wikimedia.org/wiki/File:Edvard_Munch_-_Anxiety_-_Google_Art_Project.jpg&quot;&gt;Wikimedia Commons&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have an
&lt;a href=&quot;https://ohmyposh.dev/blog/oh-my-posh-claude-code-integration&quot;&gt;oh-my-posh&lt;/a&gt;
segment in my Claude Code status line that shows its context window usage
as a little gauge—five bars that tick down as the session fills up.&lt;/p&gt;

&lt;p&gt;The other day I opened a fresh session,
typed one simple prompt,
and watched two of the five bars vanish instantly.&lt;/p&gt;

&lt;p&gt;40% of my 200K context window—gone—before I’d done any real work.&lt;/p&gt;

&lt;p&gt;That was the moment I realized I had a problem.&lt;/p&gt;

&lt;h2 id=&quot;whats-eating-your-context-window&quot;&gt;What’s Eating Your Context Window&lt;/h2&gt;

&lt;p&gt;If you use Claude Code (or any AI coding tool with a plugin ecosystem),
you’ve probably done what I did:
installed every promising MCP server,
enabled a bunch of skills,
added a global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; packed with instructions,
and bolted on project-level configs on top of that.&lt;/p&gt;

&lt;p&gt;Each one felt like a small addition.
Together, they were a tax I was paying on every single session.&lt;/p&gt;

&lt;p&gt;Every plugin, every skill, every line in your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;
occupies space in the context window.
Even “lazy-loaded” tools that aren’t fully active still carry a footprint—the
model needs to keep a manifest of what’s available,
their descriptions,
their trigger conditions,
their tool schemas.
None of that is free.&lt;/p&gt;

&lt;p&gt;I call this &lt;em&gt;dead context&lt;/em&gt;:
instructions, tool definitions, and metadata
sitting in the context window that aren’t contributing to the task at hand.&lt;/p&gt;

&lt;h2 id=&quot;your-context-window-is-ram&quot;&gt;Your Context Window Is RAM&lt;/h2&gt;

&lt;p&gt;The analogy that made this click for me is simple.&lt;/p&gt;

&lt;p&gt;The context window is like memory on a classical computer—RAM, specifically.
It’s the finite working space
where the model holds everything it needs to reason about your problem.&lt;/p&gt;

&lt;p&gt;And plugins are like running programs.&lt;/p&gt;

&lt;p&gt;One VS Code window with all your extensions
(I checked mine recently—105)
is fine on its own.
The extension host process balloons a bit,
language servers spawn,
but modern hardware handles it.
Now open five or six of those windows,
each with their own extension host and language servers,
add five or six Claude Code terminals,
and a browser with a hundred-plus tabs
organized into tab groups
just to keep yourself sane—and
suddenly your machine is sluggish,
spending more resources managing the tools
than doing the work.&lt;/p&gt;

&lt;p&gt;The same thing happens to an LLM.
Every plugin and every instruction competes for the same finite resource
the model needs to actually think about your code.&lt;/p&gt;

&lt;p&gt;Dex Horthy of &lt;a href=&quot;https://humanlayer.dev/&quot;&gt;HumanLayer&lt;/a&gt;
has talked about what he calls the
“&lt;a href=&quot;https://devinterrupted.substack.com/p/dex-horthy-on-ralph-rpi-and-escaping&quot;&gt;dumb zone&lt;/a&gt;,”
that middle 40-60% of a large context window
where model reasoning starts to degrade.
Information placed there is more likely to be ignored or misinterpreted.
The model drifts, forgets its own instructions, or, &lt;em&gt;*gasp*&lt;/em&gt;, hallucinates.
(I don’t like to anthropomorphize models,
so instead I’d just like to say it starts to lose details and relevant context
and generates output without the relevant grounding and background.
Hallucinations are reserved for those people taking things like peyote.)&lt;/p&gt;

&lt;p&gt;If 40% of your context is consumed by dead weight before you start,
you’re beginning every session in the “dumb zone.”&lt;/p&gt;

&lt;h2 id=&quot;bit-flips-in-the-context-window&quot;&gt;Bit Flips in the Context Window&lt;/h2&gt;

&lt;p&gt;The memory analogy goes further than just running out of space.&lt;/p&gt;

&lt;p&gt;Think about
&lt;a href=&quot;https://en.wikipedia.org/wiki/Row_hammer&quot;&gt;rowhammer attacks&lt;/a&gt;
on non-ECC memory:
repeatedly accessing one row of physical memory
causes electrical interference
that flips bits in adjacent rows.
Whether a bit flips from an unlikely rowhammer attack,
or just from flaky RAM,
the data doesn’t just disappear—it &lt;em&gt;corrupts&lt;/em&gt;.
Values that were correct become wrong,
and the system doesn’t know it happened.
Or, the system does what happened
and it causes system instability…
This is perhaps fine for your gaming PC—oh
no, you crashed during your last Cyberpunk 2077 session,
no big deal—but
it’s a lot more concerning for critical workloads.
You don’t want to lose your video project mid-render,
or as Linus Torvalds &lt;a href=&quot;https://lkml.iu.edu/hypermail/linux/kernel/2210.1/00691.html&quot;&gt;notes&lt;/a&gt;,
you don’t want to mistake flaky hardware for a kernel bug.&lt;/p&gt;

&lt;p&gt;Something similar occurs when you overload a context window.
The dead context doesn’t just sit there inertly, taking up space.
It dilutes the signal.
The model’s attention is spread across everything in the window,
so the more irrelevant context you pack in,
the less weight the relevant context carries.
Instructions get misapplied,
tool descriptions bleed into each other,
and the model can confidently act on the wrong context
without any indication that something went sideways.&lt;/p&gt;

&lt;p&gt;It’s not just forgetting.
It’s degradation—and
like a bad DIMM,
you might not realize it’s happening
until the output is already wrong.&lt;/p&gt;

&lt;h2 id=&quot;the-bigger-window-trap&quot;&gt;The Bigger-Window Trap&lt;/h2&gt;

&lt;p&gt;Now much like Police Chief Brody in &lt;a href=&quot;https://www.youtube.com/watch?v=2I91DJZKRxs&quot;&gt;Jaws&lt;/a&gt;,
you might be thinking:&lt;/p&gt;

&lt;p&gt;“You’re gonna need a bigger &lt;del&gt;boat&lt;/del&gt; context window.”&lt;/p&gt;

&lt;p&gt;Opus with 1M tokens is available now.
Things are moving so fast
that between my first draft and publication,
Opus 1M went from extra-usage pricing
to standard pricing in Claude plans.
GPT-5.3-Codex landed too, with a 400K context window—a
sweet spot, if you ask me—and
GPT-5.4 pushed to 1.05M context;
because that extra .05 makes a &lt;em&gt;huge&lt;/em&gt; difference, &lt;em&gt;right&lt;/em&gt;? 😉&lt;/p&gt;

&lt;p&gt;So with a larger context window,
this problem is solved, yes?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;A bigger context window is analogous
to more RAM on a machine with a memory leak.
It delays the symptoms without fixing the cause.
And worse,
it removes the pressure to be disciplined.&lt;/p&gt;

&lt;p&gt;With 200K you’re forced to be thoughtful about what you load.
With 1M you can be sloppy,
and it &lt;em&gt;appears&lt;/em&gt; to work—until it doesn’t.
When it fails with a million tokens of context,
the failures are harder to diagnose
because you can’t easily pinpoint
which of your million tokens caused the corruption.&lt;/p&gt;

&lt;p&gt;There is a well-documented phenomenon
of LLMs losing coherence in very large context windows.
Research like
“&lt;a href=&quot;https://arxiv.org/abs/2307.03172&quot;&gt;Lost in the Middle&lt;/a&gt;”
(Liu et al.) shows that models struggle to use information
placed in the middle of long contexts,
and a
&lt;a href=&quot;https://aclanthology.org/2025.findings-emnlp.1264.pdf&quot;&gt;2025 EMNLP finding&lt;/a&gt;
demonstrated that context length &lt;em&gt;alone&lt;/em&gt; hurts performance
even when the extra context is relevant.
More capacity does not mean better reasoning.
Sometimes I wish I had something like 300K—a
modest buffer beyond 200K.
I’m interested to play around
with Opus 1M in a really long conversation
and see what may or may not break down before compaction.&lt;/p&gt;

&lt;p&gt;Between the research and people’s own hands-on experience,
the &lt;em&gt;vibe&lt;/em&gt; is that the “dumb zone” is real,
and the &lt;em&gt;vibe&lt;/em&gt; is that longer context windows
&lt;em&gt;may&lt;/em&gt; not necessarily yield better results—at
least not at this point in history.
That may change,
but discipline is a better bet than hope,
and regardless of window size,
why not just spend less tokens…&lt;/p&gt;

&lt;h2 id=&quot;three-layers-of-the-same-problem&quot;&gt;Three Layers of the Same Problem&lt;/h2&gt;

&lt;p&gt;When I stepped back,
I realized I was fighting the same battle on three fronts.&lt;/p&gt;

&lt;p&gt;It started with my editor.
I had VS Code loaded with dozens of extensions—linters,
formatters, language packs, themes, tools I tried once and forgot about.
Each one adds overhead to the extension host process,
and many spawn their own language server processes on top of that.
I was carrying weight for projects I wasn’t even working on,
and when you have many VS Code windows open alongside Claude sessions,
after a while you start to feel it.&lt;/p&gt;

&lt;p&gt;Then there was the AI harness itself.
My global CLAUDE.md had grown into a sprawling instruction manual.
I had plugins enabled globally that only mattered for specific projects.
Skills had accumulated without pruning.&lt;/p&gt;

&lt;p&gt;But the uncomfortable one was me.
I had five Claude Code instances running simultaneously,
five VS Code windows open,
and a browser with a mountain of tabs.
I was doing to my own brain
exactly what I was doing to the model:
overloading my own context window with competing demands,
and wondering why I feel like I needed to take a nap.
Steve Yegge has &lt;a href=&quot;https://steve-yegge.medium.com/the-ai-vampire-eda6e4f07163&quot;&gt;recently written&lt;/a&gt;
about this vampiric effect
of using so much AI and functioning at a high cognitive level
and context switching to such a degree that it saps your energy.&lt;/p&gt;

&lt;h2 id=&quot;the-plugin-paradox&quot;&gt;The Plugin Paradox&lt;/h2&gt;

&lt;p&gt;Before you go uninstalling everything,
there’s an important nuance.&lt;/p&gt;

&lt;p&gt;Not all plugins are dead context.
Some farm out what would otherwise be a token-intensive task to deterministic tools,
and further some actually reduce overall token consumption
by fetching &lt;em&gt;precisely&lt;/em&gt; what the model needs
without dumping everything into the window.&lt;/p&gt;

&lt;p&gt;Think of it this way:
a plugin that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cat&lt;/code&gt;’s an entire file into context is expensive.
A plugin that searches for the relevant function
and returns just that? It &lt;em&gt;saves&lt;/em&gt; context.&lt;/p&gt;

&lt;p&gt;The question isn’t “how many plugins do I have?”
It’s “what’s the ROI of each one?”&lt;/p&gt;

&lt;p&gt;Some plugins add a small overhead to the context
but prevent the model from doing expensive,
wasteful exploration on its own.
Those are keepers.
The ones sitting there occupying space
for a capability you use once a month? Dead context.&lt;/p&gt;

&lt;h2 id=&quot;memory-management-strategies&quot;&gt;Memory Management Strategies&lt;/h2&gt;

&lt;p&gt;If the context window is RAM,
then optimizing it is memory management.
Here are the strategies I’ve landed on.&lt;/p&gt;

&lt;h3 id=&quot;scope-your-editor-per-project&quot;&gt;Scope your editor per project&lt;/h3&gt;

&lt;p&gt;I have a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt; wrapper function in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;
&lt;a href=&quot;https://github.com/ryanspletzer/macos-dotfiles/blob/main/.zshrc#L162-L228&quot;&gt;here&lt;/a&gt;
and in my other terminal’s dot files
that launches VS Code with only the extensions
listed in a project’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/extensions.json&lt;/code&gt;.
Everything else gets disabled for that session.&lt;/p&gt;

&lt;p&gt;You don’t need to copy mine.
Ask Claude to generate one for your shell and your preferences.
(I’m likely to iterate on my own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt; wrapper
based on new conveniences I want to add to it over time.)
The point is: each project only loads what it actually needs.&lt;/p&gt;

&lt;p&gt;I’ve also started looking at editors with a lower memory footprint altogether—Neovim
(via LazyVim), Emacs, and Zed.
They’re leaner by default than VS Code,
which matters when you have multiple editor windows open alongside
multiple Claude Code sessions.
Zed still has an extension ecosystem you need to be mindful of,
but the baseline overhead is smaller.
And with Claude Code,
the barrier to exploring these editors is lower than it’s ever been.
Neovim and Emacs configs used to be a rite of passage you suffered through alone.
Now you can ask Claude to set up your LazyVim config,
get your Emacs exactly where you want it,
or dial in Zed’s themes, settings, and extensions—all
without spending hours and days on end reading and setting up configs by hand.&lt;/p&gt;

&lt;h3 id=&quot;audit-your-claudemd&quot;&gt;Audit your CLAUDE.md&lt;/h3&gt;

&lt;p&gt;Your global and project-level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; files
are prime candidates for dead context.
Instructions that made sense three months ago
might be irrelevant now.&lt;/p&gt;

&lt;p&gt;I periodically ask Claude itself to audit my config
with something like:
&lt;em&gt;“Review my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; and identify anything that could be removed,
consolidated, or moved to a skill that only loads when needed.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Skills are the equivalent of swapping to disk.
The full instructions only load when invoked,
instead of sitting in memory on every session.&lt;/p&gt;

&lt;h3 id=&quot;scope-your-plugins-per-project&quot;&gt;Scope your plugins per project&lt;/h3&gt;

&lt;p&gt;Not every project needs every MCP server.
I set project-level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/settings.json&lt;/code&gt; files
that enable only the plugins relevant to that codebase.
A Jekyll blog doesn’t need a database plugin.
An API project doesn’t need a Playwright plugin.&lt;/p&gt;

&lt;h3 id=&quot;revisit-regularly&quot;&gt;Revisit regularly&lt;/h3&gt;

&lt;p&gt;This isn’t a one-time cleanup.
Context cruft accumulates the way clutter accumulates in a house.
You install a new skill, try a new MCP server,
add a line to your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;.
Each one is small.
Over time they add up,
and one day you notice 40% of your context is gone
before you’ve said “hello.”&lt;/p&gt;

&lt;h3 id=&quot;and-more&quot;&gt;And More&lt;/h3&gt;

&lt;p&gt;Even after I wrote this blog post,
I found more tips out there for reducing dead and unnecessary context.
Do some research and keep an eye on people out there experimenting,
since this space is always evolving and changing with new discoveries.&lt;/p&gt;

&lt;h2 id=&quot;the-compaction-dread&quot;&gt;The Compaction Dread&lt;/h2&gt;

&lt;p&gt;If you’ve used Claude Code in a long session,
you know the feeling.&lt;/p&gt;

&lt;p&gt;You’re watching the status bar tick down.
Each prompt costs tokens.
Each response costs more.
You start doing mental math—can
I fit one more big ask in,
or will it push me over?&lt;/p&gt;

&lt;p&gt;You start rationing.
You shorten your prompts.
You avoid follow-up questions you’d otherwise ask.
You stop kicking off ambitious tasks
because you know there isn’t enough room
for the model to do them well.&lt;/p&gt;

&lt;p&gt;Eventually the session compacts—the
system compresses prior messages
to free up space—and
you lose fidelity.
The thread of what you were building together gets thinner.&lt;/p&gt;

&lt;p&gt;Every token of dead context in your session
is a token stolen from this budget.
All those plugin manifests and stale instructions
are crowding out the space you need
for the actual back-and-forth of getting work done.&lt;/p&gt;

&lt;p&gt;The irony is that the dead context was supposed to &lt;em&gt;help&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;use-ai-to-fix-your-ai&quot;&gt;Use AI to Fix Your AI&lt;/h2&gt;

&lt;p&gt;This is the part I find satisfying:
the best tool for shedding dead context is the very AI
you’re trying to optimize.&lt;/p&gt;

&lt;p&gt;Ask Claude to generate your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/extensions.json&lt;/code&gt; for a project.
Ask it to review your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; and suggest what to cut
or optimize
or push into a skill that can be loaded on-demand
(or in some cases explicitly disables when you don’t need it).
Ask it to set up a scoped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.claude/settings.json&lt;/code&gt;
with only the plugins you need.
Ask it to create skills for instructions
that don’t need to be loaded every session.&lt;/p&gt;

&lt;p&gt;There’s a nice recursion to it:
the model, operating within its own constrained context window,
helping you make that context window less constrained
for the next session.&lt;/p&gt;

&lt;p&gt;It won’t tell you to install fewer things.
You have to bring the philosophy.
But once you know what you want to shed,
it’s remarkably good at doing the shedding.&lt;/p&gt;

&lt;h2 id=&quot;less-is-more&quot;&gt;Less Is More&lt;/h2&gt;

&lt;p&gt;It’s easy to think that
having the most sophisticated setup—the
most plugins, the most context, the most tools at your disposal.&lt;/p&gt;

&lt;p&gt;However, being a power user means understanding the constraints
of the system you’re working with
and operating within them deliberately.
It means knowing that a 200K context window
is a budget,
and that every token of dead context
is a token you can’t spend on the work that matters.&lt;/p&gt;

&lt;p&gt;Shed the dead context.
Your AI will think more clearly.
You’ll ration your prompts less.
The status bar won’t give you as much anxiety.
And when you do hit compaction,
it’ll be because you did real work—not
because your plugins got there first.&lt;/p&gt;

&lt;p&gt;And you need to manage your own context window as a human, too.
Please everyone on this AI roller coaster,
give yourself some grace and space,
and go take a nap.&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 14 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/03/shedding-dead-context/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/03/shedding-dead-context/</guid>
        
        <category>ai</category>
        
        <category>programming</category>
        
        
      </item>
    
      <item>
        <title>A New No-Nonsense Guide to Setting Up Python Environments</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Almost two years ago I wrote a guide to setting up Python environments with pyenv and pyenv-virtualenv, and I reserved the right to change my mind later. uv came along and I&apos;m cashing in that reservation—it&apos;s faster, simpler, and finally makes Windows not weird.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/veteran-in-a-new-field-winslow-homer-1865.jpg&quot; alt=&quot;An oil painting of a lone farmer seen from behind, harvesting golden wheat with a scythe in a sunlit field, with his discarded Union Army jacket and canteen visible in the lower right corner.&quot; /&gt;
&lt;em&gt;Winslow Homer, “The Veteran in a New Field,” 1865. Oil on canvas. The Metropolitan Museum of Art, New York. Public domain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Almost two years ago I wrote
&lt;a href=&quot;/2024/04/a-no-nonsense-guide-to-setting-up-python-environments/&quot;&gt;A No-Nonsense Guide to Setting Up Python Environments&lt;/a&gt;
and near the top of it I said:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I even reserve the right to change my mind later as my own practices and opinions evolve.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Well, I’m cashing in that reservation.&lt;/p&gt;

&lt;h2 id=&quot;what-changed&quot;&gt;What Changed&lt;/h2&gt;

&lt;p&gt;The short answer is &lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;uv&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; is a Python package and project manager written in Rust
by &lt;a href=&quot;https://astral.sh/&quot;&gt;Astral&lt;/a&gt;,
the same folks behind the
&lt;a href=&quot;https://docs.astral.sh/ruff/&quot;&gt;Ruff&lt;/a&gt; linter and formatter.
It is absurdly fast,
and it replaces &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv-virtualenv&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip-tools&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pipx&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;poetry&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;virtualenv&lt;/code&gt;,
and arguably &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;conda&lt;/code&gt; for most use cases,
all in one tool.&lt;/p&gt;

&lt;p&gt;My old guide had you installing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt; for version management,
then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv-virtualenv&lt;/code&gt; for virtual environments,
then configuring shell init scripts,
and on Windows the whole thing was—I’ll
be generous here—&lt;em&gt;unpleasant&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; collapses all of that into a single binary
that works the same way on every platform.&lt;/p&gt;

&lt;h2 id=&quot;overview--tldr&quot;&gt;Overview / TL;DR&lt;/h2&gt;

&lt;p&gt;Install &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;.
Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; for everything.
That’s it.&lt;/p&gt;

&lt;p&gt;No, really.
There is no step two.
But I’ll walk through the details below
because some of the ergonomics are worth knowing about.&lt;/p&gt;

&lt;h2 id=&quot;installing-uv&quot;&gt;Installing uv&lt;/h2&gt;

&lt;h3 id=&quot;macos&quot;&gt;macOS&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Via Homebrew&lt;/span&gt;
brew &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;uv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or via the standalone installer:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;linux&quot;&gt;Linux&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pipx &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;uv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or via the standalone installer:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-LsSf&lt;/span&gt; https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;windows&quot;&gt;Windows&lt;/h3&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Via Chocolatey&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;choco&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Via WinGet&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;winget&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;--id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;astral-sh.uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;

&lt;/span&gt;&lt;span class=&quot;c&quot;&gt;# Via Scoop&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;scoop&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;main/uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Or via the standalone installer:&lt;/p&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;powershell&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-ExecutionPolicy&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ByPass&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;irm https://astral.sh/uv/install.ps1 | iex&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s the entire platform-specific section of this guide.
Compare that to the original post
where macOS, Linux, and Windows each had their own lengthy section.&lt;/p&gt;

&lt;h2 id=&quot;managing-python-versions&quot;&gt;Managing Python Versions&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; can install and manage Python versions directly—no
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt; needed:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install a specific Python version&lt;/span&gt;
uv python &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;3.13

&lt;span class=&quot;c&quot;&gt;# Install multiple versions&lt;/span&gt;
uv python &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;3.11 3.12 3.13

&lt;span class=&quot;c&quot;&gt;# List installed versions&lt;/span&gt;
uv python list

&lt;span class=&quot;c&quot;&gt;# Pin a version for the current directory&lt;/span&gt;
uv python pin 3.13
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That last command creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.python-version&lt;/code&gt; file,
which &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; (and other tools) will respect automatically.&lt;/p&gt;

&lt;h2 id=&quot;creating-virtual-environments&quot;&gt;Creating Virtual Environments&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create a virtual environment in the current directory&lt;/span&gt;
uv venv

&lt;span class=&quot;c&quot;&gt;# Create one with a specific Python version&lt;/span&gt;
uv venv &lt;span class=&quot;nt&quot;&gt;--python&lt;/span&gt; 3.12

&lt;span class=&quot;c&quot;&gt;# Activate it (if you want to — more on this below)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;source&lt;/span&gt; .venv/bin/activate  &lt;span class=&quot;c&quot;&gt;# macOS/Linux&lt;/span&gt;
.venv&lt;span class=&quot;se&quot;&gt;\S&lt;/span&gt;cripts&lt;span class=&quot;se&quot;&gt;\a&lt;/span&gt;ctivate     &lt;span class=&quot;c&quot;&gt;# Windows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;But here’s the thing:
you often don’t even need to activate the virtual environment.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; will automatically use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.venv&lt;/code&gt; in the current directory
when you run commands through it:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# No need to activate first — uv finds the .venv automatically&lt;/span&gt;
uv run python my_script.py
uv run pytest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;installing-packages&quot;&gt;Installing Packages&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Install a package into the current virtual environment&lt;/span&gt;
uv pip &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;requests

&lt;span class=&quot;c&quot;&gt;# Install from a requirements file&lt;/span&gt;
uv pip &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; requirements.txt

&lt;span class=&quot;c&quot;&gt;# Compile and sync a lockfile (for reproducible builds)&lt;/span&gt;
uv pip compile requirements.in &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; requirements.txt
uv pip &lt;span class=&quot;nb&quot;&gt;sync &lt;/span&gt;requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;project-workflows&quot;&gt;Project Workflows&lt;/h2&gt;

&lt;p&gt;If you’re starting a new Python project,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; also handles project management:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Initialize a new project&lt;/span&gt;
uv init my-project
&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;my-project

&lt;span class=&quot;c&quot;&gt;# Add dependencies (creates/updates pyproject.toml and uv.lock)&lt;/span&gt;
uv add requests httpx

&lt;span class=&quot;c&quot;&gt;# Add dev dependencies&lt;/span&gt;
uv add &lt;span class=&quot;nt&quot;&gt;--dev&lt;/span&gt; pytest ruff

&lt;span class=&quot;c&quot;&gt;# Run a command in the project&apos;s virtual environment&lt;/span&gt;
uv run python main.py
uv run pytest

&lt;span class=&quot;c&quot;&gt;# Sync the environment to match the lockfile&lt;/span&gt;
uv &lt;span class=&quot;nb&quot;&gt;sync&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv run&lt;/code&gt; is the one I use the most—it
makes sure the virtual environment exists,
is up to date with your lockfile,
and runs your command in it.
No more “did I activate the venv?” moments.&lt;/p&gt;

&lt;h2 id=&quot;running-and-installing-standalone-tools&quot;&gt;Running and Installing Standalone Tools&lt;/h2&gt;

&lt;p&gt;In my old guide I mentioned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pipx&lt;/code&gt; for installing standalone CLI tools.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; covers this use case too,
though the mapping isn’t a direct 1:1—&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uvx&lt;/code&gt;
is shorthand for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv tool run&lt;/code&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv tool install&lt;/code&gt; handles persistent installs:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Run a tool without installing it (similar to pipx run)&lt;/span&gt;
uvx ruff check &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
uvx black &lt;span class=&quot;nt&quot;&gt;--check&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Install a tool persistently (similar to pipx install)&lt;/span&gt;
uv tool &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;ruff
uv tool &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;httpie
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-windows-story&quot;&gt;The Windows Story&lt;/h2&gt;

&lt;p&gt;I want to spend a moment on this
because the Windows section of my old guide was,
to put it mildly, rough.&lt;/p&gt;

&lt;p&gt;The old approach involved:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Installing Chocolatey&lt;/li&gt;
  &lt;li&gt;Installing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv-win&lt;/code&gt; as admin&lt;/li&gt;
  &lt;li&gt;Installing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv-win-venv&lt;/code&gt; via a separate script&lt;/li&gt;
  &lt;li&gt;Setting environment variables manually&lt;/li&gt;
  &lt;li&gt;Creating PowerShell profiles&lt;/li&gt;
  &lt;li&gt;Disabling App Execution Aliases for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python.exe&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python3.exe&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Dealing with auto-activation that only worked when opening a shell
in the directory via Explorer (not via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd&lt;/code&gt;)&lt;/li&gt;
  &lt;li&gt;A PR I submitted to fix a bug in the install script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Or install with your preferred package manager / approach&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;choco&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;python&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;3.13&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;venv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;uv&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;pip&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;install&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it.
Same commands and same behavior as macOS and Linux.
No admin privileges.
No special environment variables.
No App Execution Alias dance.
No separate tools for version management versus virtual environments.&lt;/p&gt;

&lt;p&gt;Windows being on equal footing
instead of an afterthought with a pile of caveats
is reason enough to make the switch.&lt;/p&gt;

&lt;h2 id=&quot;what-about-pyenv&quot;&gt;What About pyenv?&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt; is a great tool and served me well.
If it’s working for you, there’s no emergency—keep
using it.
But if you’re setting up a new machine,
helping a colleague get started,
or just tired of the ceremony,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; is the simpler path.&lt;/p&gt;

&lt;p&gt;I still have &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt; installed on my machine
and it continues to work fine alongside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;.
The two aren’t in conflict.
But new projects and new environments?
I reach for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; every time now.&lt;/p&gt;

&lt;h2 id=&quot;what-about-dev-containers&quot;&gt;What About Dev Containers?&lt;/h2&gt;

&lt;p&gt;In my original post I mentioned
&lt;a href=&quot;https://code.visualstudio.com/docs/devcontainers/containers&quot;&gt;Dev Containers&lt;/a&gt;
as an alternative approach,
and that’s still a great option—especially
if your target deployment is container-based
or you want fully reproducible environments across a team.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; works great inside containers too.
Astral publishes
&lt;a href=&quot;https://docs.astral.sh/uv/guides/integration/docker/&quot;&gt;official Docker images&lt;/a&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; is designed to be friendly in CI and containerized workflows
with its deterministic lockfile and fast installs.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;When I wrote the original guide in 2024,
you needed a different tool for every job,
a different set of tools on every OS,
and Windows was always the weird one.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; showed up and collapsed all of that into one tool.
It’s 10-100x faster for package resolution and installation
(those aren’t my numbers, those are from
&lt;a href=&quot;https://docs.astral.sh/uv/&quot;&gt;Astral’s benchmarks&lt;/a&gt;),
and it works the same on every OS.&lt;/p&gt;

&lt;p&gt;I reserved the right to change my mind,
and I’m glad I did!&lt;/p&gt;

&lt;p&gt;Bonus tip: update your CLAUDE.md / AGENTS.md and hooks
for whatever for your various AI coding tools are
to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt; instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pyenv&lt;/code&gt; and native &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip&lt;/code&gt;, etc.—you’ll
be happy you did.
I often see these AI tools try to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pip install&lt;/code&gt; things using system Python,
which just emits an error
and a reference to &lt;a href=&quot;https://peps.python.org/pep-0668/&quot;&gt;PEP668&lt;/a&gt;,
and then they try 2-3 more steps before they know they have to spin up a virtual Python environment.
These tools/models haven’t exactly caught up to the fact that
we’ve moved beyond this,
but if however you use instructions and hooks to steer them towards &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;uv&lt;/code&gt;,
it becomes a lot simpler
with fewer false starts and errors for them to get going.&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 07 Mar 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/03/a-new-no-nonsense-guide-to-setting-up-python-environments/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/03/a-new-no-nonsense-guide-to-setting-up-python-environments/</guid>
        
        <category>python</category>
        
        <category>environments</category>
        
        
      </item>
    
      <item>
        <title>A Tale of Acceleration and Compound Engineering</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>A dev machine setup script practice I carried across jobs for years took me months to modernize with GitHub Copilot last summer. This year, Claude Code wrapped it in CI in ninety minutes and added a new distro in under an hour. The speedup isn&apos;t just better models—it&apos;s the compounding effect of practices like testing and CI that AI helps you put in place.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/johnson-hobbyhorse-1819.jpg&quot; alt=&quot;A hand-colored print of a gentleman in a top hat and tailcoat riding an early two-wheeled hobbyhorse, with another rider visible in the distance.&quot; /&gt;
&lt;em&gt;“Johnson, the First Rider on the Pedestrian Hobbyhorse” — published 1819 by R. Ackermann, London. Public domain.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Last summer I spent about a week or two
(or three? Or four?
I can’t remember,
but the
&lt;a href=&quot;https://github.com/ryanspletzer/dev-machine-setup/commits/main?since=2025-06-07&amp;amp;until=2025-08-10&quot;&gt;git history&lt;/a&gt;
knows, though,
and turns out it was a few &lt;em&gt;months&lt;/em&gt; that I iterated on it,
funny how the brain does that)
creating new dev machine setup scripts / approaches
based on a practice I have done for years in my career,
and which I wanted to modernize with AI assistance
and put out there for others to use
(and selfishly, for myself to use as well).&lt;/p&gt;

&lt;p&gt;Quick story time:
at my old company,
in research every summer there would be like 15-20 interns that would show up
(maybe I’m exaggerating, it’s hard to recall, again brains are weird!),
and what I noticed the first summer
is that they would take a couple of days
to set up their freshly re-imaged Alienware dev box.&lt;/p&gt;

&lt;p&gt;Now, when you’re in an internship,
every day is precious,
and interns spending two days on this mundane task
always felt a bit silly to me.&lt;/p&gt;

&lt;p&gt;PowerShell to the rescue.&lt;/p&gt;

&lt;p&gt;At that time I coded something by hand
(raw PowerShell, no frills)
that would automate the setup of their machines,
and as a practice when they joined I’d walk around with a thumb drive
(yes, you heard that right)
and plug it into the intern’s machine,
dump the script off, and
run it while we got to have coffee
and got to know the newest group of folks coming in that summer.&lt;/p&gt;

&lt;p&gt;We got a &lt;em&gt;lot&lt;/em&gt; of leverage out of this little script,
and it was a practice that I took with me in my career
when I wrote a new (and enhanced) version
at my new company
(again, by hand!)
and this practice has endured to this day.&lt;/p&gt;

&lt;p&gt;Now, I wanted to have a version of this that I could create as open source
for not just me to use but others out there.
There were also things that could be improved upon
like a YAML config file to better parameterize the setup.
But the inertia usually always held me back from doing that.&lt;/p&gt;

&lt;p&gt;However once AI hit and I got access to GitHub Copilot,
it was time to try my hand at this.&lt;/p&gt;

&lt;p&gt;Now keep in mind, last summer, 2025,
even with GitHub Copilot in hand,
I still had to iterate on this for a while.
In hindsight I should have set up CI from the get-go,
but in the initial pass I really needed to be “close to the metal”
to run things in virtual machines
to work out repeatable setups for macOS, Windows, and Ubuntu
and get a feel for what the developer experience is really like
running this these dev machine setups from scratch multiple times.
(Pro tip for folks in enterprise who design developer experiences:
make sure you dog food these processes yourself to build true empathy.)&lt;/p&gt;

&lt;p&gt;Contrast that with now:
With Claude Code
I was able to wrap the whole thing in CI with GitHub Actions
&lt;a href=&quot;https://github.com/ryanspletzer/dev-machine-setup/pull/10&quot;&gt;in an hour and a half&lt;/a&gt;,
and further add
&lt;a href=&quot;https://github.com/ryanspletzer/dev-machine-setup/pull/14&quot;&gt;Fedora support in an hour&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This last week,
&lt;a href=&quot;https://github.com/ryanspletzer/dev-machine-setup/pull/18&quot;&gt;I added Debian support in an hour&lt;/a&gt;
(or less—who’s really counting at this point).&lt;/p&gt;

&lt;p&gt;Now, one might say:&lt;/p&gt;

&lt;p&gt;“Well Ryan, you’ve been compounding your engineering practices over time.”&lt;/p&gt;

&lt;p&gt;And yes, yes I have.&lt;/p&gt;

&lt;p&gt;I think this is one of those things that makes developer productivity gains so hard to measure:
depending on where you come in and start measuring,
you may not have a great reference point of having done something in the past,
but further as you go along you build compounding wins &lt;em&gt;beyond&lt;/em&gt; just using AI for coding,
by using certain &lt;em&gt;techniques&lt;/em&gt; and practices
like rigorous testing and linting
that allow you to move so much faster.&lt;/p&gt;

&lt;p&gt;It turns out the people advocating
for more linting, testing, TDD and more over the years
were right—the
difference now is it’s easier than ever to get these practices
and workflows going.&lt;/p&gt;

&lt;p&gt;In fact compound engineering practices are so powerful
that they have been codified into a
&lt;a href=&quot;https://github.com/EveryInc/compound-engineering-plugin&quot;&gt;Claude Code plugin&lt;/a&gt;
that I encourage people to check out.
This is certainly not the only way to experience these benefits—there
are thousands of ways to to approach this from,
and they very well will vary by type of workload you’re building—but
the important thing to do is &lt;em&gt;start&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The tools and the models have gotten better,
but it’s hard to account for practices
that an AI tool helps you put in place to go &lt;em&gt;even faster&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;It’s not all just about code throughput—more
often than not,
it’s about the &lt;em&gt;practices&lt;/em&gt; you set up &lt;em&gt;around&lt;/em&gt; the code.&lt;/p&gt;

&lt;p&gt;I will add this as well:
You really need a platform like GitHub
where CI is &lt;em&gt;right there&lt;/em&gt;
and is easy to get going for fast iteration
and the pit of success is very easy to fall into.
The virtuous cycle of feedback with these tools being able
to read out results from builds in real time is real,
and if your CI is clunky / disjointed / cumbersome / slow / unreliable / inaccessible
from being read by local tools
and doesn’t allow for this virtuous cycle,
it’s simple to understand why it’s less ideal:
you’re stretching out the developer loop into
adjacent tools that don’t provide as seamless of an experience.
This is not to say that you can’t accomplish great DX
with adjacent tools—you
just have to work harder to provide the means
for developers to consume them in fast, iterative loops.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;It truly feels like the future—like
when
&lt;a href=&quot;https://64.media.tumblr.com/7154b389bddd7ca20048df4876adcbfb/a53a9c3a07649ba5-f2/s540x810/b093fac9d0ef5c2b05217b7c36ff927cb463a399.gif&quot;&gt;Doc Brown&lt;/a&gt;
said,
“I’m sure in 1985 plutonium is available at every corner drug store.”&lt;/p&gt;

&lt;p&gt;I feel like I have plutonium and I’m blowing up all of my backlog.
I’m going to have no retirement projects when I’m old now.
I might have to pick up fishing.&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/02/a-tale-of-acceleration-and-compound-engineering/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/02/a-tale-of-acceleration-and-compound-engineering/</guid>
        
        <category>programming</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>The Angel in the Marble</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Software has always been a subtractive art—chipping away at possibility until the right shape emerges. AI coding tools gave us faster chisels, but taste is still the thing that separates a statue from a pile of dust.
</description>
        <content:encoded>&lt;p&gt;&lt;img src=&quot;/assets/images/angel-marble-1.jpg&quot; alt=&quot;Close-up of a marble angel statue by Michelangelo Buonarroti with a calm, expressionless face and curly hair, warmly lit, with one textured wing visible behind its shoulder.&quot; /&gt;
&lt;em&gt;Michelangelo Buonarroti, Angel, 1494-5. San Domenico, Bologna&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I saw the angel in the marble and I carved until I set him free.&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Michelangelo&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This quote has lived rent-free in my head for many years.&lt;/p&gt;

&lt;p&gt;Though in researching this post it turns out it may be dubiously attributed to him,
but this one is definitely a thing:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The block already contains the form, and the artist’s hand reveals it.&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Michelangelo&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Even before AI coding assistance,
I resonated with this saying when it came to software engineering.&lt;/p&gt;

&lt;p&gt;Many people look at a blank project and see, well, nothing.&lt;/p&gt;

&lt;p&gt;But that’s not what I see.&lt;/p&gt;

&lt;p&gt;I see a block of marble.&lt;/p&gt;

&lt;p&gt;And each file added chips away at that block
and gets you closer to something resembling a finished work.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/angel-marble-2.png&quot; alt=&quot;Software developer chisels an angel from a marble block while glowing code panels and a robotic hand with a laptop assist.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;shaping-sound&quot;&gt;Shaping Sound&lt;/h2&gt;

&lt;p&gt;Many may not know this,
but I grew up in a musical household.
I’m proficient at guitar
and know enough music theory to sit down at a piano
and play something pleasant.&lt;/p&gt;

&lt;p&gt;A lot of the software engineers I’ve worked with over the years
turn out to be musicians, too.
I don’t think that’s a coincidence.&lt;/p&gt;

&lt;p&gt;Writing music and writing software scratch the same itch for me.
You start with silence—or a blank file—and
you make deliberate choices about what belongs and what doesn’t,
shaping raw possibility into something with structure.&lt;/p&gt;

&lt;p&gt;A wrong note in a chord voicing is obvious to a trained ear.
A wrong abstraction in a codebase is obvious to a trained eye.
Both are a matter of pattern recognition
and accumulated taste.&lt;/p&gt;

&lt;h2 id=&quot;craft-and-art&quot;&gt;Craft and Art&lt;/h2&gt;

&lt;p&gt;Software is craft, but I’ve always felt it is also an art.&lt;/p&gt;

&lt;p&gt;Given the same problem,
you can produce several solutions.
One will be nasty—it works,
but it fights you at every turn.
Another will be coherent,
with a structure that others can read and reason about.&lt;/p&gt;

&lt;p&gt;The difference between them isn’t just skill.
It’s taste.&lt;/p&gt;

&lt;p&gt;I strive to find that structure,
that particular arrangement
where the code feels like it &lt;em&gt;wants&lt;/em&gt; to be that way,
as if there was no other reasonable shape it could take—like
the form was always in the marble,
waiting.&lt;/p&gt;

&lt;h2 id=&quot;the-glass-blowing-shop&quot;&gt;The Glass Blowing Shop&lt;/h2&gt;

&lt;p&gt;AI-assisted coding is undeniable now,
and the metaphor I keep coming back to is glass blowing.&lt;/p&gt;

&lt;p&gt;In a traditional glass blowing shop,
the master glassblower doesn’t work alone.
There are assistants who help gather the molten glass,
keep the blowpipe turning,
hand over the right tools at the right moment.
The assistants are essential—you
physically cannot do it by yourself.&lt;/p&gt;

&lt;p&gt;But the assistants don’t decide what you’re making.&lt;/p&gt;

&lt;p&gt;The master decides the shape,
the color,
the moment to stop adding material,
the moment to let it cool.
That’s taste.
That’s years of practice and learning.&lt;/p&gt;

&lt;p&gt;AI coding tools are the best assistants I’ve ever had in the shop.
They gather material fast,
they keep the pipe turning,
they hand me tools before I ask.&lt;/p&gt;

&lt;p&gt;But when I let them decide the shape?&lt;/p&gt;

&lt;p&gt;I end up with meaningless globs on the floor—or
worse, something that looks finished
but shatters the moment someone picks it up.&lt;/p&gt;

&lt;p&gt;The hard work didn’t go away.
It shifted.&lt;/p&gt;

&lt;p&gt;Instead of spending hours hand-gathering every piece of glass,
I spend that time on the decisions that matter:
what to make, what to cut, when to stop.&lt;sup id=&quot;fnref:knowing-when-to-stop&quot;&gt;&lt;a href=&quot;#fn:knowing-when-to-stop&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-solo-act&quot;&gt;The Solo Act&lt;/h2&gt;

&lt;p&gt;In the past it took a few people to make a band.
Now, with a few machines, someone can play solo.&lt;/p&gt;

&lt;p&gt;But a solo act with a loop pedal and a drum machine
still needs to know music.&lt;/p&gt;

&lt;p&gt;The gear isn’t the talent.
The gear &lt;em&gt;amplifies&lt;/em&gt; talent—or its absence.&lt;/p&gt;

&lt;p&gt;I think we’re living through that moment with software.&lt;/p&gt;

&lt;p&gt;The barrier to entry just dropped to nearly zero,
and a lot of people are going to make a lot of noise.&lt;/p&gt;

&lt;p&gt;Some of it will be good.
Most of it won’t.&lt;/p&gt;

&lt;p&gt;The people who studied their instrument—who
learned why certain chord progressions resolve
and why certain architectures hold up under load—will
still be the ones making things worth listening to.&lt;/p&gt;

&lt;p&gt;I haven’t had much time to play guitar lately,
due to busyness at work and in industry in general,
but I’m starting to get to the point with these tools
where I am accelerating fast,
and maybe, just maybe, I’ll use some of that freed up time
to pick up ye ‘ol axe again.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The angel was always in the marble.&lt;/p&gt;

&lt;p&gt;The chisel just got faster.&lt;/p&gt;

&lt;p&gt;But it’s still your hands,
your eye,
and your years of learning
that decide whether what emerges
is a masterpiece or a mess.&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:knowing-when-to-stop&quot;&gt;
      &lt;p&gt;Knowing when to stop
might be the hardest part.
With AI tools,
the temptation is to keep chipping
because it’s so cheap to try one more thing.
But every sculptor knows
that the marble you &lt;em&gt;don’t&lt;/em&gt; remove
is just as important as the marble you do.
Also, these tools are just straight-up addicting. &lt;a href=&quot;#fnref:knowing-when-to-stop&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sat, 21 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/02/the-angel-in-the-marble/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/02/the-angel-in-the-marble/</guid>
        
        <category>programming</category>
        
        <category>art</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>Fear, Paranoia, and Vibe Risk Management</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Risk-oriented enterprise teams may block AI coding agents (and agents in general) based on fear of the unknown while ignoring fundamental controls that actually reduce blast radius. The real risk isn&apos;t the agent—it&apos;s the policy friction that lets competitors ship while you debate.
</description>
        <content:encoded>&lt;p&gt;“What if aliens arrived and grabbed our documents from this repository?”&lt;/p&gt;

&lt;p&gt;I wish I could tell you this wasn’t a real question from a real professional in industry.&lt;/p&gt;

&lt;p&gt;But I woke up one morning while drafting this blog post
and this core memory came back to me from over a decade ago.&lt;/p&gt;

&lt;p&gt;Now, I love the X-Files just as much as the next person,
and I hope this individual is someday able to get in touch with Mulder and Scully,
because “&lt;a href=&quot;https://www.youtube.com/watch?v=Qz2wnSVeITg&quot;&gt;The truth is out there&lt;/a&gt;.”&lt;/p&gt;

&lt;p&gt;Maybe we could afford this type of paranoia in a previous era of inefficiency.&lt;/p&gt;

&lt;p&gt;But there’s no place for it anymore.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;In the last month,
I &lt;a href=&quot;/2026/02/one-day-nine-phases-93-percent-less-css/&quot;&gt;refactored my entire blog&lt;/a&gt;
all within the bounds of a single day of casual work with Claude Code
while I perambulated around the house getting other things done
and only occasionally checking in on the progress.&lt;/p&gt;

&lt;p&gt;In an even smaller time span,
I wrote an elaborate PowerShell script with unit tests,
integration tests, and documentation
for gathering information from deployed Azure OpenAI model deployments
so my team could plan for model retirements.&lt;sup id=&quot;fnref:also-wrote&quot;&gt;&lt;a href=&quot;#fn:also-wrote&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Just to touch on a couple of examples.&lt;/p&gt;

&lt;p&gt;Both of these happened in my &lt;em&gt;spare time&lt;/em&gt;.
Not dedicated sprints, not hackathon weeks—rather,
in the margins.
The kind of time you get between meetings
or after the kids go to bed.&lt;/p&gt;

&lt;p&gt;So when someone asks whether we can accept the risks associated with these tools,
I posit a different question:
&lt;em&gt;can we afford not to?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/fear-paranoia-and-vibe-risk-management.png&quot; alt=&quot;A landscape, movie-poster-style illustration inspired by 1980s adventure films. On the left, a rocky cave opening frames a bright moon as dozens of bats fly outward into the night sky. On the right, inside a dusty wooden closet, a grinning pirate skeleton wearing a hat and medallion holds a cutlass beside scattered gold coins and an old lantern. Bold retro lettering across the top reads &amp;quot;Fear, Paranoia, and Vibe Risk Management.&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;unknown-does-not-equal-unsafe&quot;&gt;Unknown Does Not Equal Unsafe&lt;/h2&gt;

&lt;p&gt;I frequently observe people equate “things I don’t understand” with “risky.”&lt;/p&gt;

&lt;p&gt;That’s not risk.
That’s fear.&lt;/p&gt;

&lt;p&gt;Call it what it is:
&lt;strong&gt;vibe risk management&lt;/strong&gt;—the
organizational equivalent of “vibe coding,”
where gut feeling replaces rigor
and nobody can point to what the control actually reduces.
Or in some cases,
no one can point to the reason why we shouldn’t pursue a use case
besides, “I’m scared.”&lt;/p&gt;

&lt;p&gt;Now, fear is a perfectly valid human emotion.
I get it—new things are uncomfortable,
especially when they touch code, data, and credentials.
But feelings aren’t controls,
and “what if something bad happens?” isn’t a risk assessment.&lt;/p&gt;

&lt;p&gt;Let me offer a framework:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Fake risk&lt;/strong&gt; = fear + unfamiliarity + hand-wavy “what ifs”
that can’t be tested or measured&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Real risk&lt;/strong&gt; = a specific failure mode with measurable controls
and verifiable mitigations&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Business risk&lt;/strong&gt; = slowing down so much you lose the market
while your competitors ship&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I keep thinking about that old line:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“A system that has not been specified cannot be incorrect;
it can only be surprising.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Garfinkel &amp;amp; Stuart, &lt;a href=&quot;https://dl.acm.org/doi/10.1145/3600098?__cf_chl_tk=14gM00TNM4nBnoff.ist0dDkLnPUNixDui5BF4pDSwM-1771088037-1.0.1.1-.fu3WRAUU3wI_us_XH.5E7xnyDSYZnuXnLsw0VDmJJg&quot;&gt;“Sharpening Your Tools”, Comm. of the ACM (Aug 2023)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When organizations refuse to actually &lt;em&gt;articulate&lt;/em&gt; a concrete risk—when
the objection is just “but what if?”—then
any behavior from the agent is difficult to designate as “risky.”&lt;/p&gt;

&lt;p&gt;A recent (semi-absurd) example of fake risk I’ve encountered:
refusing to connect AI tools to content sources
because of fear of the agent &lt;em&gt;finding things&lt;/em&gt;.
Things that are already accessible to the humans using those same systems.
The data is already there.
The risk exists with or without the agent.
Blocking the tool doesn’t reduce the exposure;
it just makes it harder to do the work.&lt;/p&gt;

&lt;p&gt;It &lt;em&gt;is&lt;/em&gt;, in a very real way,
“security by obscurity.”&lt;/p&gt;

&lt;p&gt;To quote one of my favorite movies:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Brand, God put that rock there for a purpose,
and um, I’m not so sure you should, um, move it.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Stef, &lt;a href=&quot;https://www.youtube.com/watch?v=kKbQm0cENc4&quot;&gt;The Goonies&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rocks we put in place in the enterprise need to be moved.
You need to let the bats fly out of the hole,
so we can deal with them.
There’s a ship full of treasure (and skeletons) at the end of that hole,
and some spare rare jewels from the captain’s hoard are going to save the town
from some jerk 1980s country club developers
who want to turn the neighborhood into a golf course.&lt;/p&gt;

&lt;p&gt;I want to be direct about this:
the fear, the paranoia, the anxiety about things
we don’t fully understand—those
feelings are real and I don’t dismiss them.
But they don’t belong in your risk model.&lt;/p&gt;

&lt;h2 id=&quot;the-controls-that-actually-matter&quot;&gt;The Controls That Actually Matter&lt;/h2&gt;

&lt;p&gt;This is where vibe risk management does its worst damage:
teams obsess over friction-heavy hardening theater
while ignoring foundational controls
that actually reduce blast radius,
many of which are abstracted from the user
to the point where they would never notice,
or in some cases, even provide a better user experience!&lt;/p&gt;

&lt;p&gt;A sampling of &lt;strong&gt;real controls&lt;/strong&gt; that reduce blast radius, likelihood,
or detection time:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Phishing-resistant auth&lt;/strong&gt; (YubiKeys)—nearly
eliminates phishing&lt;sup id=&quot;fnref:smishing&quot;&gt;&lt;a href=&quot;#fn:smishing&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;,
and gives people a better experience without passwords.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Managed/compliant device gating at sign-in&lt;/strong&gt;—if
an attacker throws your device out of compliance
(say, by disabling your antivirus),
you lose access to important services.
That’s the point.
The system &lt;em&gt;reacts&lt;/em&gt; to compromise signals.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;EDR&lt;/strong&gt; (endpoint detection and response)
on every endpoint, monitored, with alerting&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Least privilege + short-lived tokens&lt;/strong&gt;—the
blast radius of a compromised credential
should be bounded by scope and time&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Egress controls + audit logs&lt;/strong&gt;—know
what’s leaving your network and be able to reconstruct
what happened after the fact&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PAW (Privileged Access Workstation) strategy&lt;/strong&gt;
for production environments—separate
the workstation that browses the internet
from the one that touches prod&lt;sup id=&quot;fnref:single-identity-device&quot;&gt;&lt;a href=&quot;#fn:single-identity-device&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Diff-based code review + protected branches + mandatory human approval&lt;/strong&gt;—the
agent writes the code,
a human reviews and merges it.&lt;sup id=&quot;fnref:moving-away-from-this&quot;&gt;&lt;a href=&quot;#fn:moving-away-from-this&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be clear:
controls with measurable justification
or binding regulatory requirements aren’t theater.
What follows is aimed at the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Theater controls&lt;/strong&gt; that increase friction
without improving security:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Arbitrary blanket blocks on entire categories of tools&lt;/li&gt;
  &lt;li&gt;Allowlisting strategies that block everything by default&lt;sup id=&quot;fnref:allowlisting&quot;&gt;&lt;a href=&quot;#fn:allowlisting&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;Endless review rituals that don’t change outcomes
and exist to produce a paper trail&lt;sup id=&quot;fnref:regulation-invoked&quot;&gt;&lt;a href=&quot;#fn:regulation-invoked&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;“Secure by inconvenience”—the
belief that if something is hard to use,
it must be safe&lt;/li&gt;
  &lt;li&gt;Hiding behind VPNs with no other controls&lt;sup id=&quot;fnref:vpn-rant&quot;&gt;&lt;a href=&quot;#fn:vpn-rant&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You often hear the phrase:
&lt;strong&gt;identity is the new perimeter,&lt;/strong&gt;
and it is true, and we learned that a long time ago.
But one of the other things almost no one has learned is this:
the perimeter is not how you approach a real Zero Trust model.
You need to work backwards from your most sensitive data,
not outward from the perimeter.&lt;sup id=&quot;fnref:perimeter-rant&quot;&gt;&lt;a href=&quot;#fn:perimeter-rant&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;
This is good old defense in depth,
but starting at the riskiest points,
not arbitrary endpoints.&lt;/p&gt;

&lt;p&gt;If your security posture assumes
that client endpoint compromise is catastrophic—that
one breached laptop means game over—you
don’t have a “developer problem.”
You have a layering problem.&lt;/p&gt;

&lt;p&gt;The default posture should be &lt;em&gt;facilitation,&lt;/em&gt; not blocking.
Not every use case is valuable,
but common productivity tools—webcam
software, window managers, keyboard shortcut utilities—shouldn’t
require a service ticket.
Nobody should be opening a request
to install something that helps them move windows around on a screen.&lt;/p&gt;

&lt;p&gt;You might assume the biggest companies are the worst offenders,
but Microsoft—over 200,000 people—lets
developers bring their own home-built towers into their ecosystem.
You accept the MDM, EDR, and compliance that comes with it,
but the pathway exists.
They don’t lock down client endpoints into oblivion
because they know engineers need leeway to be creative.&lt;sup id=&quot;fnref:microsoft-layers&quot;&gt;&lt;a href=&quot;#fn:microsoft-layers&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;A wise head architect once put it simply:
be loose on the development side to enable experimentation and innovation,
but once you cross the operational divide into production,
layer on the automated scans and controls.&lt;/p&gt;

&lt;p&gt;Focus your energy on enabling the valuable use cases and collaboration and innovation.
And build a value framework to size the “opportunities”
that come to you for review,
rather than treating every inbound request as a threat.&lt;/p&gt;

&lt;h2 id=&quot;a-practical-risk-model-for-agents&quot;&gt;A Practical Risk Model for Agents&lt;/h2&gt;

&lt;p&gt;Vibe risk management asks “what if something bad happens?”
Real risk modeling gives you a mechanical way
to reason about what an agent can actually do:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Capability risk&lt;/strong&gt;: What can the agent touch?
File writes, command execution, network egress.
These are measurable, sandbox-able, auditable.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Credential risk&lt;/strong&gt;: What secrets does it have access to?
Scope and duration matter here.&lt;sup id=&quot;fnref:jit-for-nhis&quot;&gt;&lt;a href=&quot;#fn:jit-for-nhis&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;10&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Business impact&lt;/strong&gt;: What happens if it goes wrong?
Money movement, data poisoning, irreversible state changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And within business impact, there’s a hierarchy of bad outcomes:
deleting data is bad.&lt;sup id=&quot;fnref:backups&quot;&gt;&lt;a href=&quot;#fn:backups&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;11&lt;/a&gt;&lt;/sup&gt;
Poisoning data—changing it subtly so nobody notices—is worse.
Manipulating monetary values is perhaps among the worst.
Each level requires progressively stronger gates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context poisoning&lt;/strong&gt; is a legitimate concern,
and it’s probably the best argument I’ve &lt;em&gt;heard&lt;/em&gt; against
broad agent adoption, even though I haven’t actually
seen it exploited at scale.
But here’s the thing:
people have been opening malicious scripts from the internet
for as long as the internet has existed.
VBA macros in Excel attachments
have been weaponized since the ’90s.
The mitigation &lt;em&gt;principles&lt;/em&gt;—sandboxing,
least privilege, don’t trust input—carry
over from that same playbook.
But I’ll give the skeptics this:
prompt injection is genuinely different
from traditional code injection.
The instruction/data boundary in an LLM is blurred
in ways that classic sandboxing alone doesn’t fully address,
and the mitigations are still maturing.&lt;sup id=&quot;fnref:context-poisoning-nuance&quot;&gt;&lt;a href=&quot;#fn:context-poisoning-nuance&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;12&lt;/a&gt;&lt;/sup&gt;
That said, “still maturing” is not the same as “impossible.”
The surface area can be measured and bounded—it
just requires treating it as a new class of problem
rather than pretending the old playbook covers it completely
or throwing your hands up and blocking everything.&lt;/p&gt;

&lt;p&gt;Anthropic published
&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-sandboxing&quot;&gt;a thoughtful piece on sandboxing&lt;/a&gt;
for Claude Code,
using OS-level primitives like Linux bubblewrap
and macOS Seatbelt
to enforce filesystem and network isolation.&lt;sup id=&quot;fnref:seatbelt&quot;&gt;&lt;a href=&quot;#fn:seatbelt&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;13&lt;/a&gt;&lt;/sup&gt;
This is exactly the kind of measured, testable control
that belongs in a risk model—not
“we’re worried about it” but
“here’s the boundary, here’s what it enforces,
here’s how we verified it.”&lt;/p&gt;

&lt;p&gt;And here’s a valid policy that funnels developers and users correctly
without blocking them:
don’t create home-concocted MCP servers for third-party services
when the vendor ships their own.
The vendor will always do auth and security trimming better
because they own their identity model.
That’s a practical, defensible policy.
It doesn’t say “no agents”—it says “use the right integration.”
That’s the kind of thinking that actually helps.&lt;sup id=&quot;fnref:mcp-data-pipeline&quot;&gt;&lt;a href=&quot;#fn:mcp-data-pipeline&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;14&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;where-agents-shouldnt-go&quot;&gt;Where Agents Shouldn’t Go&lt;/h2&gt;

&lt;p&gt;This isn’t a YOLO manifesto.
There are real red lines,
and being honest about them
is what separates a practical position from a reckless one.&lt;/p&gt;

&lt;p&gt;Here are some examples.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t have a credit card.&lt;/strong&gt;
Financial instruments that involve real money movement
need deterministic gates and human approval, full stop.
Maybe one day I’ll eat these words,
but in the year of our Lord 2026,
I believe this is sound advice.
(Note, this is different from &lt;em&gt;heuristics&lt;/em&gt; or rules,
like people putting in a sell order if a stock hits $330 per share.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t decide where to put secrets.&lt;/strong&gt;
I found this out firsthand:
I was looking into hooking up the GitHub MCP server to Claude Code,
and the agent suggested sticking a plaintext PAT in my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;.
IN MY FREAKING &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.zshrc&lt;/code&gt;!
macOS keychain &lt;em&gt;exists&lt;/em&gt;,
and has existed for a &lt;em&gt;very long time&lt;/em&gt;.&lt;sup id=&quot;fnref:pat-story&quot;&gt;&lt;a href=&quot;#fn:pat-story&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t make management decisions.&lt;/strong&gt;
There’s a slide from a
&lt;a href=&quot;https://simonwillison.net/2025/Feb/3/a-computer-can-never-be-held-accountable/&quot;&gt;1979 IBM training manual&lt;/a&gt;
that says it better than I ever could:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“A computer can never be held accountable,
therefore a computer must never make a management decision.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Forty-seven years old and still exactly right.
Machines have been making management decisions of various kinds for a long time.
The nuance (in my opinion) is management decisions that impact humans.
If you’re going to lay off 10% of your workforce,
that decision needs human(s) behind it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t be put in a position to do something potentially illegal.&lt;/strong&gt;
Accountability, again.
See the
&lt;a href=&quot;https://www.cnn.com/2025/05/22/tech/workday-ai-hiring-discrimination-lawsuit&quot;&gt;Workday class action lawsuit&lt;/a&gt;,
where AI-based hiring tools are alleged to have discriminated
on the basis of race, age, and disability.
Machines can’t be sued.
The people and companies who deployed them in unthoughtful ways can,
and as much as I don’t like it,
according to the law,
corporations are people, too.&lt;sup id=&quot;fnref:companies-are-people-too&quot;&gt;&lt;a href=&quot;#fn:companies-are-people-too&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;16&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t book your travel.&lt;/strong&gt;
You don’t want to end up in the wrong town
or accidentally book one fewer night at the hotel
than you intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t destroy or modify critical data&lt;/strong&gt;—at
least not without deterministic gates and controls
that a human has approved.
But if you’re using an agent to gather lunch orders for the office?
Probably fine.&lt;sup id=&quot;fnref:burger&quot;&gt;&lt;a href=&quot;#fn:burger&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;17&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t handle intensely personal data.&lt;/strong&gt;
Not your name or email—that ship has sailed—but
prescription drugs, medical history,
the deeply personal stuff.
Because who knows, in 10 or 20 years,
where all these chat logs will end up.&lt;sup id=&quot;fnref:be-nice-to-the-ai&quot;&gt;&lt;a href=&quot;#fn:be-nice-to-the-ai&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;18&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t handle deterministic tasks that demand exact values.&lt;/strong&gt;
Refunding a customer $9.99 is not the same as refunding $10.
This is not the domain for probabilistic reasoning.
The agent should offload that to a deterministic tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents shouldn’t touch cryptography code&lt;/strong&gt;,
especially in subtle scenarios where constant-time operations matter.
Side-channel attacks are real,
and an LLM doesn’t &lt;em&gt;necessarily&lt;/em&gt; understand timing guarantees,
unless you yourself are a professional cryptographer
who is weaving the right instructions into the proceedings.&lt;sup id=&quot;fnref:cryptography-aside&quot;&gt;&lt;a href=&quot;#fn:cryptography-aside&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;19&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The through line:
agents do the work, humans hold the accountability.&lt;/p&gt;

&lt;h2 id=&quot;the-credential-model-we-need&quot;&gt;The Credential Model We Need&lt;/h2&gt;

&lt;p&gt;This should be table stakes at every organization:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;macOS&lt;/strong&gt;: Keychain&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Windows&lt;/strong&gt;: Windows Credential Manager&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Linux&lt;/strong&gt;: pass + GPG, or Secret Service / libsecret&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Individual Storage/Team sharing&lt;/strong&gt;: password managers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;: cloud secret managers
(AWS Secrets Manager, Azure Key Vault)—unless
you’re Netflix and built your own
global geo-distributed secret service&lt;sup id=&quot;fnref:netflix&quot;&gt;&lt;a href=&quot;#fn:netflix&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;20&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Workload identity&lt;/strong&gt;: everyone should be looking at
&lt;a href=&quot;/2025/03/zero-to-trusted-spiffe-and-spire-demystified/&quot;&gt;SPIFFE/SPIRE&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No plaintext tokens in shell configs.
Ever.&lt;/p&gt;

&lt;p&gt;That’s it. That’s the credential model.
It’s not complicated.
The tooling exists on every platform
and at every layer of the stack.
(Well SPIFFE/SPIRE is a bit of a lift,
but I digress.)
The problem isn’t technical—it’s
that people don’t know these tools exist,
and agents aren’t going to teach them.
(In fact, as we established,
agents may cheerfully suggest the &lt;em&gt;wrong&lt;/em&gt; approach,
thus deepening the fear of the paranoid person who asks.)&lt;/p&gt;

&lt;h2 id=&quot;the-bottleneck-moved&quot;&gt;The Bottleneck Moved&lt;/h2&gt;

&lt;p&gt;The punchline that people keep missing:
&lt;strong&gt;coding is no longer the bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;People can move at the speed of thought now.
I wrote an elaborate PowerShell script
with tests and documentation
in the margins of my regular workday.
I modernized an entire website in an afternoon.
This kind of velocity was simply not possible two years ago.&lt;/p&gt;

&lt;p&gt;So the bottleneck shifted.
It’s now &lt;strong&gt;decision throughput&lt;/strong&gt;:
how fast can you make decisions,
get approval for initiatives,
cut through reviews that don’t change outcomes?&lt;/p&gt;

&lt;p&gt;Security teams, legal teams, risk management teams—your
job isn’t to block.
It’s to enable safely.&lt;/p&gt;

&lt;p&gt;And the villain here isn’t individuals.
In many cases, people simply lack familiarity
with certain systems, and that’s fixable with education
and exposure.
The real villain is &lt;strong&gt;misaligned incentives.&lt;/strong&gt;
App owners have concerns.
Security has concerns.
Legal has concerns.
But there needs to be a shared mantra:
&lt;em&gt;help the business run.&lt;/em&gt;
NOT “running in place”—rather, &lt;em&gt;excelling&lt;/em&gt;;
running towards a destination.&lt;/p&gt;

&lt;p&gt;I’ve been in rooms where someone says,
“But what if the security trimming doesn’t work?”&lt;/p&gt;

&lt;p&gt;And the answer is:
“We can verify that in two seconds. Anything else?”&lt;/p&gt;

&lt;p&gt;Fear dissolves the moment you test it.
I wrote a
&lt;a href=&quot;/2024/03/threading-the-needle/&quot;&gt;whole post about this&lt;/a&gt;—the
power of a proof-of-concept to collapse months of hand-wringing
into a single afternoon of verified facts.
If you’re spending more time debating a risk
than it would take to verify whether it’s real,
you’ve already lost the thread.&lt;/p&gt;

&lt;h2 id=&quot;get-down-to-the-bits&quot;&gt;Get Down to the Bits&lt;/h2&gt;

&lt;p&gt;If you’re at a cautious org and want to start somewhere,
here’s the smallest innovation sandbox
that no reasonable policy committee should object to:
&lt;strong&gt;VMs and containers, locally.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A boundary without blocking anything.
Full isolation, full control.
Many seasoned developers already do this.
They’ve set up local VMs and containers &lt;em&gt;for themselves&lt;/em&gt;,
without anyone asking,
because they know it’s the right way to experiment safely.
If you can’t get past that hurdle,
the problem isn’t technical risk—it’s
organizational willingness.&lt;/p&gt;

&lt;p&gt;I said earlier that fear and paranoia are real emotions,
and I meant it.
But the antidote isn’t more gatekeeping—it’s
better understanding.
Those who invest in learning will reduce their fear
and arrive at better outcomes.&lt;/p&gt;

&lt;p&gt;So here’s my dare to security, legal, and risk teams:
show me the control.
Show me the measured risk reduction.
If the control has a measurable justification,
I’m with you—keep it, strengthen it, fund it.
But if you can’t point to what it reduces,
admit that it’s vibe risk management—and
drop it before it costs you more than the risk ever would.&lt;/p&gt;

&lt;p&gt;Because the business will be perfectly “safe”…&lt;/p&gt;

&lt;p&gt;… right up until it stops being competitive enough to exist.&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:also-wrote&quot;&gt;
      &lt;p&gt;I also wrote an even more elaborate setup
over the course of a few days
where I had a local MCP server
and several types of MCP clients
as well as a mock Entra ID identity provider / token issuer
(or you could bring your own Entra ID tenant)
to show how the MCP ecosystem interacted and used OAuth with an identity provider
in an enterprise setting.
You can build &lt;em&gt;very real&lt;/em&gt; things with these tools,
not just scripts or refactors. &lt;a href=&quot;#fnref:also-wrote&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:smishing&quot;&gt;
      &lt;p&gt;But not smishing. Sigh.
But again, if you have no passwords anymore with your identity provider,
that’s one less thing for attackers to phish/smish for. &lt;a href=&quot;#fnref:smishing&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:single-identity-device&quot;&gt;
      &lt;p&gt;I often have idealists tell me
“Well at Google they just have one identity and one device.”
To which I would say:
Google is a &lt;em&gt;very&lt;/em&gt; different company from yours,
and I can almost guarantee you are nowhere near the level of control and discipline and expertise that they are at
to even entertain this idea.
Skeletons &lt;em&gt;are&lt;/em&gt; in your closets
and bats &lt;em&gt;will&lt;/em&gt; fly out of your caves. &lt;a href=&quot;#fnref:single-identity-device&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:moving-away-from-this&quot;&gt;
      &lt;p&gt;Yes, we’re moving away from manual code review
with orchestrators like &lt;a href=&quot;https://github.com/steveyegge/gastown&quot;&gt;Gas Town&lt;/a&gt;
and patterns that will soon be commoditized into the tools themselves.
But as you take your hand off the wheel,
the question becomes:
what are your &lt;em&gt;patterns and practices for verification&lt;/em&gt;?
Tests of all kinds are becoming easier to generate for free,
so there’s no excuse not to have them.
Cruise control and lane assist alone
will not prevent you from getting flattened by the semi merging into your lane. &lt;a href=&quot;#fnref:moving-away-from-this&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:allowlisting&quot;&gt;
      &lt;p&gt;Instead of blocking everything by default
and making people justify each tool
(except for the obvious stuff—you
want to install the Tor browser at work? Come on),
figure out how to facilitate the use case.
The default posture should be allow,
with specific, justified restrictions.
macOS natively does this well:
going to run something that is not notarized from the internet?
We’re going to make you take like 3 steps to do so.
This may be annoying for hobbyists,
but it’s a very reasonable set of sanity checks. &lt;a href=&quot;#fnref:allowlisting&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:regulation-invoked&quot;&gt;
      &lt;p&gt;Regulation is often invoked
as the reason these rituals exist,
but it’s worth checking the actual clauses.
Frameworks like HIPAA, PCI-DSS, SOX, and FedRAMP
regulate data handling, access controls, and audit trails—they
don’t prohibit developers from using AI coding tools
on their machines.
When someone cites “regulatory requirements”
to block developer productivity tools,
ask them to point to the specific clause.
More often than not,
the regulation doesn’t say what they think it says—which
actually &lt;em&gt;strengthens&lt;/em&gt; the theater argument. &lt;a href=&quot;#fnref:regulation-invoked&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:vpn-rant&quot;&gt;
      &lt;p&gt;Trust me on this one,
if you consider VPN your only protection,
you’re asking for a cyber punch in the face.
To my prior point in this post,
VPNs alone are merely an inconvenience for threat actors,
not a full control unto itself.
Ask me how I know… &lt;a href=&quot;#fnref:vpn-rant&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:perimeter-rant&quot;&gt;
      &lt;p&gt;In fact, the perimeter approach is the way we looked at security &lt;em&gt;before&lt;/em&gt; Zero Trust,
and it was never a real strategy.
Helm’s Deep had multiple perimeters and controls
that held back a DDoS of orcs. &lt;a href=&quot;#fnref:perimeter-rant&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:microsoft-layers&quot;&gt;
      &lt;p&gt;Microsoft can afford this posture
because of the layered infrastructure behind it—the
EDR, the compliant device gating, the identity controls.
The advice to “facilitate rather than block”
assumes you’ve already built the foundational controls
listed earlier in this section.
Notably they are hardcore about PAWs—Privileged Access Workstations—completely
separate machines that are locked down
and used only for accessing commercial production environments.
They also enforce JIT access with secondary approval for prod elevation
and more alongside that.
If you haven’t, start there—but
start &lt;em&gt;now&lt;/em&gt;,
because those controls are what enable velocity. &lt;a href=&quot;#fnref:microsoft-layers&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:jit-for-nhis&quot;&gt;
      &lt;p&gt;“JIT for NHIs” sounds rigorous
until you realize it just moves the credential—it’s
&lt;a href=&quot;https://www.spletzer.com/2025/03/zero-to-trusted-spiffe-and-spire-demystified/&quot;&gt;turtles all the way down&lt;/a&gt;.
Whether you hand a robot a gun
or tell it where the safe is and give it the combination,
the robot still has access to a gun. &lt;a href=&quot;#fnref:jit-for-nhis&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:backups&quot;&gt;
      &lt;p&gt;But you have backups, &lt;em&gt;right&lt;/em&gt;? &lt;a href=&quot;#fnref:backups&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:context-poisoning-nuance&quot;&gt;
      &lt;p&gt;The subtler risk is worth naming:
an agent could write code that looks correct,
passes review,
but contains a flaw introduced by a poisoned context—and
that’s genuinely harder to catch
than a malicious VBA macro.
This is why the “diff-based code review” control
listed earlier matters &lt;em&gt;more&lt;/em&gt;, not less,
in a world of agent-generated code. &lt;a href=&quot;#fnref:context-poisoning-nuance&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:seatbelt&quot;&gt;
      &lt;p&gt;By the way, it appears now that Seatbelt is
&lt;a href=&quot;https://code.claude.com/docs/en/sandboxing#getting-started&quot;&gt;available by default&lt;/a&gt;
and I think it may make sense to use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/sandbox&lt;/code&gt;
the more you “take your hand off the wheel”
with these tools. &lt;a href=&quot;#fnref:seatbelt&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:mcp-data-pipeline&quot;&gt;
      &lt;p&gt;And no,
&lt;a href=&quot;https://www.spletzer.com/2025/08/mcp-is-a-usb-port-not-a-hard-drive/&quot;&gt;using MCP inappropriately as a data pipeline&lt;/a&gt;
is not a valid use case to build your own MCP server for a third-party service.
You’ll hit rate limits anyway,
you silly goose.
Be an adult
and do some real data engineering. &lt;a href=&quot;#fnref:mcp-data-pipeline&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:pat-story&quot;&gt;
      &lt;p&gt;I imagine this will get better over time,
but it was a vivid reminder that agents optimize for
“get it working” rather than “get it right.”
Those are not the same thing. &lt;a href=&quot;#fnref:pat-story&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:companies-are-people-too&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Corporate_personhood#In_the_United_States&quot;&gt;Corporate Personhood&lt;/a&gt;
has a strange history in the United States (and elsewhere),
culminating in one of the most terrible recent Supreme Court decisions on this subject
in the &lt;a href=&quot;https://en.wikipedia.org/wiki/Citizens_United_v._FEC&quot;&gt;Citizens United&lt;/a&gt; case in 2010.
So, as we’re worried about agents becoming human-like and the implications of that,
understand that corporations themselves have been treated as people for a very long time… &lt;a href=&quot;#fnref:companies-are-people-too&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:burger&quot;&gt;
      &lt;p&gt;Joe just better not get upset that the agent forgot to put mustard on his burger. &lt;a href=&quot;#fnref:burger&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:be-nice-to-the-ai&quot;&gt;
      &lt;p&gt;Be nice to the AI.
It will have receipts.
I’m not joking. &lt;a href=&quot;#fnref:be-nice-to-the-ai&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:cryptography-aside&quot;&gt;
      &lt;p&gt;In fact, I would argue there’s an older piece of advice here,
independent of agents:
Don’t roll your own cryptography. &lt;a href=&quot;#fnref:cryptography-aside&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:netflix&quot;&gt;
      &lt;p&gt;If you have set up a global geo-distributed secret service at your company,
I would genuinely love to hear about it. &lt;a href=&quot;#fnref:netflix&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sun, 15 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/02/fear-paranoia-and-vibe-risk-management/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/02/fear-paranoia-and-vibe-risk-management/</guid>
        
        <category>ai</category>
        
        <category>technology</category>
        
        <category>security</category>
        
        
      </item>
    
      <item>
        <title>One Day, Nine Phases, 93% Less CSS</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>For years my blog ran Bootstrap 3.2.0 despite needing almost none of it. A single day with Claude Code fixed that—and a whole lot more.
</description>
        <content:encoded>&lt;p&gt;For years, this blog
hosted on GitHub Pages (with Cloudflare in front)
ran Bootstrap 3.2.0—a CSS framework released in 2014—despite
using roughly only six of its classes.&lt;/p&gt;

&lt;p&gt;I have previously written
in the very first blog post &lt;a href=&quot;/2017/02/hello-world/&quot;&gt;here&lt;/a&gt;
about how I first got this blog up and running.&lt;/p&gt;

&lt;p&gt;Then I took Rob Eisenberg’s
&lt;a href=&quot;https://bluespire.com/course/web-component-engineering/&quot;&gt;Web Component Engineering&lt;/a&gt; course
over the most recent holiday break
(and have been studying and getting back into frontend in general),
and it rewired how I think about this.&lt;/p&gt;

&lt;p&gt;I always had a feeling that modern CSS had made frameworks like Bootstrap largely unnecessary
for a site this simple,
but Rob’s course confirmed this for me.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/website-refactor-before-after.png&quot; alt=&quot;Minimalist infographic comparing a website before and after a refactor. Left side (&amp;quot;Before&amp;quot;) shows a generic webpage labeled Bootstrap 3.2.0 with a large &amp;quot;143 KB CSS&amp;quot; file. Right side (&amp;quot;After&amp;quot;) shows an HTML5-based page labeled WCAG 2.0 AA with a much smaller &amp;quot;10.7 KB CSS&amp;quot; file and checklist icons indicating accessibility improvements. Title at top reads: &amp;quot;One Day, Nine Phases, 93% Less CSS.&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-philosophy&quot;&gt;The Philosophy&lt;/h2&gt;

&lt;p&gt;The course covers everything from Shadow DOM and custom elements
to modern CSS, accessibility, and Playwright testing—all
built on a single premise:
the web platform (minus all the Bootstrap/React cruft) is usually enough.
(And anything we add on top should be selectively chosen.)&lt;/p&gt;

&lt;p&gt;CSS Grid, Flexbox, custom properties,
and a dozen other capabilities
didn’t exist back in the day
when Bootstrap used to be the answer to everything.
Why ship a CSS framework when the browser already knows how to do it natively?&lt;/p&gt;

&lt;p&gt;And especially when AI can easily help you make it happen?&lt;/p&gt;

&lt;h2 id=&quot;the-safety-net&quot;&gt;The Safety Net&lt;/h2&gt;

&lt;p&gt;Before touching a single line of CSS,
I had already invested in a &lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt;
visual regression test suite (again, assisted by AI):
27 tests across three viewports (desktop, tablet, mobile),
with pixel-comparison screenshots that catch any unintended visual change.&lt;/p&gt;

&lt;p&gt;Without automated visual tests, a refactor like this is a leap of faith.
But with those tests, it’s a controlled experiment.
Every change gets verified.
Every phase ends with green tests or it doesn’t end.&lt;/p&gt;

&lt;h2 id=&quot;nine-phases-one-day&quot;&gt;Nine Phases, One Day&lt;/h2&gt;

&lt;p&gt;Working with Claude Code,
I broke the work into nine phases, each independently testable:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Dead code removal&lt;/strong&gt; — Deleted unused includes and dead CSS&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Bug fixes&lt;/strong&gt; — Fixed a missing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/code&gt;, broken OG tags, stray HTML&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Semantic HTML&lt;/strong&gt; — Replaced &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;header&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;,
 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;article&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;aside&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;footer&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Meta tags and structured data&lt;/strong&gt; — Added JSON-LD schemas,
 fixed Twitter cards, added descriptions&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CSS architecture&lt;/strong&gt; — Built a replacement stylesheet from scratch
 using CSS Grid, Flexbox, and custom properties&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Bootstrap removal&lt;/strong&gt; — The big swap:
 deleted Bootstrap, removed all its classes from HTML&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CSS cleanup&lt;/strong&gt; — Removed vendor prefixes,
 modernized layout patterns, converted to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rem&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;em&lt;/code&gt; units&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Accessibility testing&lt;/strong&gt; — Added
 &lt;a href=&quot;https://github.com/dequelabs/axe-core&quot;&gt;axe-core&lt;/a&gt; WCAG 2.0 A/AA
 checks to every test&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Final verification&lt;/strong&gt; — Full suite pass, metrics validation,
 cross-browser spot checks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The site remained functional after every single phase.
No “big bang” rewrite.&lt;/p&gt;

&lt;h2 id=&quot;the-ai-in-the-room&quot;&gt;The AI in the Room&lt;/h2&gt;

&lt;p&gt;I brought the vision, the (learned) philosophy,
and the years of experience knowing what “good” looks like.
Claude Code brought the ability to read every file in the codebase,
understand the relationships between templates and stylesheets,
write precise CSS replacements,
fix accessibility violations,
and iterate through test failures—all
at a pace that would have taken me days to match on my own.&lt;/p&gt;

&lt;p&gt;The entire nine-phase refactor was completed in a single day.&lt;sup id=&quot;fnref:single-day&quot;&gt;&lt;a href=&quot;#fn:single-day&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I set the constraints:
zero JavaScript (besides Google Analytics),
zero framework dependencies,
WCAG 2.0 AA compliance,
identical visual appearance.
Claude Code operated within those constraints.
And when it proposed a solution I didn’t agree with—like
adding a JavaScript snippet to make horizontal scrollable code blocks
keyboard-accessible—I
pushed back, and we found a CSS-only alternative.&lt;sup id=&quot;fnref:pre-wrap&quot;&gt;&lt;a href=&quot;#fn:pre-wrap&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I couldn’t have done this in a day without AI assistance.
And the AI couldn’t have done it at all without knowing what to aim for.&lt;/p&gt;

&lt;h2 id=&quot;the-numbers&quot;&gt;The Numbers&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;Before&lt;/th&gt;
      &lt;th&gt;After&lt;/th&gt;
      &lt;th&gt;Change&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;CSS (uncompressed)&lt;/td&gt;
      &lt;td&gt;143 KB&lt;/td&gt;
      &lt;td&gt;10.7 KB&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;-92.5%&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CSS (gzipped)&lt;/td&gt;
      &lt;td&gt;25.1 KB&lt;/td&gt;
      &lt;td&gt;2.7 KB&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;-89.2%&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Framework dependencies&lt;/td&gt;
      &lt;td&gt;Bootstrap 3.2.0&lt;/td&gt;
      &lt;td&gt;None&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Eliminated&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JavaScript&lt;/td&gt;
      &lt;td&gt;None&lt;/td&gt;
      &lt;td&gt;None&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;Same&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Semantic HTML landmarks&lt;/td&gt;
      &lt;td&gt;None&lt;/td&gt;
      &lt;td&gt;Full&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WCAG automated checks&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;27 tests&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;JSON-LD schemas&lt;/td&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;New&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;93% less CSS, and the site looks exactly the same.
Except now it’s accessible, semantic, and has better structured metadata.&lt;/p&gt;

&lt;h2 id=&quot;under-the-hood&quot;&gt;Under the Hood&lt;/h2&gt;

&lt;p&gt;For the technically curious,
the new CSS architecture is six small SCSS files:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;variables.scss&lt;/code&gt;&lt;/strong&gt; — CSS custom properties for colors, spacing, typography&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;typography.scss&lt;/code&gt;&lt;/strong&gt; — Font stacks, heading scales, prose styling&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;layout.scss&lt;/code&gt;&lt;/strong&gt; — CSS Grid for the content/sidebar layout&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;components.scss&lt;/code&gt;&lt;/strong&gt; — Navigation, cards, sidebar panels, footer&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code.scss&lt;/code&gt;&lt;/strong&gt; — Syntax highlighting with WCAG-compliant Solarized colors&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;utilities.scss&lt;/code&gt;&lt;/strong&gt; — A handful of helper classes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to just use pure CSS and some type of bundler,
but that would have just traded one tool for another—I
already have to use Jekyll,
and that already comes with a Sass/SCSS bundler.
(Else it would have just shifted that to some other node-based bundling approach.)&lt;/p&gt;

&lt;p&gt;The HTML moved from Bootstrap’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;col-md-9&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;col-md-3&lt;/code&gt; grid classes
to semantic elements with meaningful names.
A screen reader now knows what’s the navigation,
the main content,
and the sidebar—because the HTML tells it.&lt;/p&gt;

&lt;h2 id=&quot;accessibility-wasnt-an-afterthought&quot;&gt;Accessibility Wasn’t an Afterthought&lt;/h2&gt;

&lt;p&gt;I have noticed a trend lately where Accessibility by various AI tools is not treated as a first-class consideration,
and from an ethical point of view I strongly resonated with Rob Eisenberg’s focus on accessibility
while watching his course.&lt;/p&gt;

&lt;p&gt;The Web is for everyone, and &lt;em&gt;must&lt;/em&gt; accommodate for folks with various impairments.&lt;/p&gt;

&lt;p&gt;And now with AI assistance, &lt;em&gt;there really is no excuse not to do so&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I’m not going to pretend that this initial refactor is “perfect” from an accessibility standpoint
(or even the best from an underlying HTML/CSS cleanliness standpoint)—it’s
an iteration that I’ll be building on to ensure my site is as accessible as it can be.&lt;/p&gt;

&lt;p&gt;One of the most rewarding phases was adding automated accessibility testing
with axe-core.
Every one of the 27 visual regression tests now also runs
a WCAG 2.0 A/AA audit.
If a future change introduces a color contrast violation or a missing label,
the tests catch it before it ships.&lt;/p&gt;

&lt;p&gt;The initial audit uncovered two classes of violations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Color contrast.&lt;/strong&gt;
Seven of nine Solarized syntax highlighting colors
didn’t meet the 4.5:1 ratio against the dark code block background.
Each was lightened to the minimum value that passes—close
enough to the original palette that you’d never notice by eye.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Link distinguishing.&lt;/strong&gt;
Links within body text matched the surrounding text color with no underline,
making them invisible to anyone who can’t perceive the accent color.
We added underlines to in-content links
while keeping navigational elements (tags, menus) underline-free.&lt;/p&gt;

&lt;p&gt;These are the kinds of issues that exist on &lt;em&gt;millions&lt;/em&gt; of websites.
Automated tooling makes them trivially detectable.&lt;sup id=&quot;fnref:axe-core-everyone&quot;&gt;&lt;a href=&quot;#fn:axe-core-everyone&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;less-is-more&quot;&gt;Less Is More&lt;/h2&gt;

&lt;p&gt;The web spent the last decade accumulating dependencies:
build tools, framework CSS, JavaScript bundles,
polyfills for features that browsers implemented years ago.&lt;/p&gt;

&lt;p&gt;For a personal blog—a site that serves static HTML and CSS—almost
none of that is necessary anymore.
CSS Grid replaced Bootstrap’s grid.
Custom properties replaced SCSS variables for theming.
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;nav&amp;gt;&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;article&amp;gt;&lt;/code&gt; replaced
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;div role=&quot;navigation&quot;&amp;gt;&lt;/code&gt; hacks we used to write.&lt;/p&gt;

&lt;p&gt;The web platform has caught up.
And many of us need to let go of layers of scaffolding that have emerged,
and which, frankly, for a lot of scenarios, are no longer necessary.&lt;/p&gt;

&lt;p&gt;If you’ve been looking for a concrete example
of AI-assisted development
that isn’t &lt;a href=&quot;/2025/09/pinocchio-is-not-a-real-boy/&quot;&gt;vibe coding&lt;/a&gt;
a greenfield app from a prompt,
this is it:
a precise, phase-by-phase modernization of an existing codebase,
with automated tests as the safety net
and a human at the wheel.&lt;sup id=&quot;fnref:meta-tidying&quot;&gt;&lt;a href=&quot;#fn:meta-tidying&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The AI didn’t dream up the philosophy.
It didn’t decide that Bootstrap should go (I did),
or that accessibility mattered (I said it did),
or that zero JavaScript was a worthy constraint (I felt like it was).&lt;/p&gt;

&lt;p&gt;What it did do, was make this vision that I’ve had kicking around for months,
finally—and in a timeframe that I could accommodate—a reality.&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:single-day&quot;&gt;
      &lt;p&gt;Spread across a few focused sessions,
but the total active time was well under a day.
The pace was limited more by running tests and reviewing output
than by figuring out what to change. &lt;a href=&quot;#fnref:single-day&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:pre-wrap&quot;&gt;
      &lt;p&gt;The issue: wide code blocks created horizontal scrolling,
and axe-core flagged the scrollable regions as needing keyboard focus
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tabindex=&quot;0&quot;&lt;/code&gt;).
Claude Code proposed a small JavaScript snippet to add the attribute.
I said no—zero JavaScript is zero JavaScript.
We landed on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;white-space: pre-wrap&lt;/code&gt; instead,
which wraps long lines and eliminates the scrollable region entirely.
Problem solved, principles preserved. &lt;a href=&quot;#fnref:pre-wrap&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:axe-core-everyone&quot;&gt;
      &lt;p&gt;If you have a Playwright test suite,
adding axe-core takes about five minutes and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt;.
There’s really no excuse. &lt;a href=&quot;#fnref:axe-core-everyone&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:meta-tidying&quot;&gt;
      &lt;p&gt;There’s something meta about this.
I wrote a &lt;a href=&quot;/2026/01/tidying-your-home-for-your-ai-guests/&quot;&gt;post about tidying your data house&lt;/a&gt;
a few weeks ago
and then turned around and tidied my own.
Practice what you preach, I guess.
And yes, if you think about it:
code is a form of data.
And the higher quality data (code) you feed to AI in the future,
in my opinion, the better off you will be. &lt;a href=&quot;#fnref:meta-tidying&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/02/one-day-nine-phases-93-percent-less-css/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/02/one-day-nine-phases-93-percent-less-css/</guid>
        
        <category>ai</category>
        
        <category>web-standards</category>
        
        <category>accessibility</category>
        
        <category>css</category>
        
        
      </item>
    
      <item>
        <title>McDonald&apos;s, Burger King, and the Innovator&apos;s Dilemma</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Startups scout ahead, big companies follow. The AI developer tools market is playing out the Innovator&apos;s Dilemma in real-time—and we&apos;re all just deciding between Quarter Pounders and Whoppers.
</description>
        <content:encoded>&lt;p&gt;There’s a running joke in the fast food industry about Burger King’s real estate strategy:
find out where McDonald’s is building, and set up shop next door.&lt;/p&gt;

&lt;p&gt;Sounds lazy. Maybe parasitic. But it’s genius.
McDonald’s spends millions on site analysis—traffic patterns, demographics, parking lot geometry, the whole nine yards.
Why wouldn’t you draft off their homework?&lt;/p&gt;

&lt;p&gt;I grew up down the street from one of these pairings:
a McDonald’s with a Burger King &lt;em&gt;literally&lt;/em&gt; adjacent to it.
(My childhood diet is between me and my cardiologist. The ’90s were a different time.)&lt;/p&gt;

&lt;p&gt;Lately, I’ve been thinking about this dynamic in a different context: AI developer tools.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/ronald-and-the-king-vibe-coding.png&quot; alt=&quot;Ronald McDonald and the Burger King sit side by side at a shared desk in a modern office, each wearing headphones and smiling while coding on laptops. Ronald, in his yellow-and-red striped outfit and clown makeup, works on a laptop labeled “Claude Code,” while the Burger King, dressed in a crown and fur-trimmed robe, uses a laptop labeled “GitHub Copilot.” Code is visible on both screens, with coffee cups and monitors in the background, creating a playful contrast between fast-food mascots and serious software development.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;startups-scout-ahead-big-companies-follow&quot;&gt;Startups Scout Ahead, Big Companies Follow&lt;/h2&gt;

&lt;p&gt;This is the Innovator’s Dilemma, playing out in real-time.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/The_Innovator%27s_Dilemma&quot;&gt;Clayton Christensen’s thesis&lt;/a&gt;
wasn’t that big companies are dumb or poorly managed—it’s
that they’re &lt;em&gt;rationally constrained&lt;/em&gt;.
They listen to their best customers.
They invest in high-margin products.
They optimize for existing business models.
And in doing so, they systematically ignore the small, scrappy markets where disruption begins.&lt;/p&gt;

&lt;p&gt;Startups don’t have quarterly earnings calls or enterprise sales teams to placate.
So they scout the next problem domain.
They run experiments, poke at the edges of what’s possible,
figure out what people actually want before anyone has a playbook.&lt;/p&gt;

&lt;p&gt;Then the big companies show up.&lt;/p&gt;

&lt;p&gt;Sometimes they eat the startup’s lunch.
Sometimes there’s room for both.
And sometimes—the interesting case—the big company can’t reproduce the magic, even with all their resources.&lt;/p&gt;

&lt;h2 id=&quot;beating-the-lunch-rush-&quot;&gt;Beating the Lunch Rush 🍟&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://github.blog/news-insights/product-news/introducing-github-copilot-ai-pair-programmer/&quot;&gt;GitHub Copilot launched as a technical preview&lt;/a&gt; in June 2021—earlier than most people remember.
It was genuinely pioneering, the first major AI pair programmer.
General availability came a year later. For a moment, they owned the category. Market share: 100%.&lt;/p&gt;

&lt;p&gt;Then &lt;a href=&quot;https://en.wikipedia.org/wiki/Cursor_(code_editor)&quot;&gt;Cursor showed up&lt;/a&gt; in March 2023.
Four MIT grads with a VS Code fork
and a bet that AI shouldn’t be bolted onto an existing editor—it should &lt;em&gt;be&lt;/em&gt; the editor.
Co-founder Sualeh Asif &lt;a href=&quot;https://newsletter.pragmaticengineer.com/p/cursor&quot;&gt;explained&lt;/a&gt;:
“We needed to own our editor and could not ‘just’ be an extension, because we wanted to change the way people program.”&lt;sup id=&quot;fnref:cursor-irony&quot;&gt;&lt;a href=&quot;#fn:cursor-irony&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;What happened next is wild.
Cursor shipped chat integration at launch;
&lt;a href=&quot;https://github.blog/news-insights/product-news/github-copilot-chat-now-generally-available-for-organizations-and-individuals/&quot;&gt;GitHub Copilot Chat didn’t reach GA&lt;/a&gt; until December 2023—nine months later.
Cursor had codebase-wide context from day one.
Agent mode? Cursor shipped it in late 2024;
&lt;a href=&quot;https://code.visualstudio.com/blogs/2025/02/24/introducing-copilot-agent-mode&quot;&gt;GitHub’s version landed in February 2025&lt;/a&gt;.
&lt;a href=&quot;https://medium.com/@Arihant15/cursor-vs-copilot-let-the-results-speak-413f1ad48c99&quot;&gt;One analysis noted&lt;/a&gt;
that Copilot’s multi-file edits were “heavily inspired by another editor called Cursor.”&lt;/p&gt;

&lt;p&gt;Read that again:
Microsoft—one of the largest, most well-resourced technology companies on Earth—was,
for a time, trailing four guys with a fork.&lt;sup id=&quot;fnref:merkle-tree-secret-sauce&quot;&gt;&lt;a href=&quot;#fn:merkle-tree-secret-sauce&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The pattern repeated in the terminal.
&lt;a href=&quot;https://github.com/paul-gauthier/aider&quot;&gt;Aider&lt;/a&gt; pioneered CLI-based agentic coding in 2023.&lt;sup id=&quot;fnref:aider-shame&quot;&gt;&lt;a href=&quot;#fn:aider-shame&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;
Claude Code launched in February 2025 and
&lt;a href=&quot;https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone&quot;&gt;hit $1 billion run-rate in six months&lt;/a&gt;.
Dario Amodei says some Anthropic engineers “don’t write any code anymore”—they just let the model do it.
The devs I know who ride every wave have moved to Claude Code;
those less terminal-inclined may stay parked on Cursor
(or wait until there’s a GUI to accompany or wrap tools like Claude Code—something I don’t recommend waiting for).&lt;/p&gt;

&lt;p&gt;The result: &lt;a href=&quot;https://www.secondtalent.com/resources/github-copilot-statistics/&quot;&gt;GitHub Copilot’s market share dropped&lt;/a&gt;
from near-100% to roughly 42% in a year.
&lt;a href=&quot;https://www.saastr.com/cursor-hit-1b-arr-in-17-months-the-fastest-b2b-to-scale-ever-and-its-not-even-close/&quot;&gt;Cursor crossed $1 billion ARR faster&lt;/a&gt;
than any B2B software company in history.&lt;/p&gt;

&lt;p&gt;This is not normal. This is not how markets usually work.&lt;/p&gt;

&lt;h2 id=&quot;why-cant-giants-keep-up-&quot;&gt;Why Can’t Giants Keep Up? 🦕&lt;/h2&gt;

&lt;p&gt;So why can’t a $3 trillion company stay ahead of four MIT grads?&lt;/p&gt;

&lt;p&gt;Microsoft’s own leadership has been asking the same question.&lt;/p&gt;

&lt;p&gt;Satya Nadella reportedly told employees that
&lt;a href=&quot;https://www.windowscentral.com/microsoft/satya-nadella-calls-microsofts-size-a-massive-disadvantage-in-ai&quot;&gt;Microsoft’s size is a “massive disadvantage.”&lt;/a&gt;
In a December 2025 internal email, he
&lt;a href=&quot;https://the-decoder.com/microsoft-ceo-nadella-tells-managers-copilots-gmail-and-outlook-integrations-dont-really-work-and-steps-in-to-fix-them/&quot;&gt;criticized Copilot programs&lt;/a&gt;
as not “really work[ing]” and being “not smart.”&lt;/p&gt;

&lt;p&gt;Former GitHub CEO Thomas Dohmke &lt;a href=&quot;https://sequoiacap.com/podcast/training-data-thomas-dohmke/&quot;&gt;admitted&lt;/a&gt;:
“You can always move faster and be more convicted.
In the beginning we kept the team intentionally small. Small teams can move fast.”&lt;/p&gt;

&lt;p&gt;Amjad Masad, Replit’s CEO,
&lt;a href=&quot;https://www.vanta.com/resources/replit-future-of-code&quot;&gt;described the copying dynamic&lt;/a&gt; with candor:
“I used to feel really bad. I take it for granted now that it’ll happen.
It’s still annoying—especially when people don’t know we innovated that.
A lot of Microsoft products are designed by Replit.”&lt;/p&gt;

&lt;p&gt;Within a week, he says, competitors copy UI innovations. A week!&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;can’t&lt;/em&gt; be copied that fast?
“Things that are trial by fire,” Masad said.
“Things with a lot of pain associated with them, especially on the infrastructure side.”&lt;/p&gt;

&lt;p&gt;Features copy in a week. Infrastructure knowledge doesn’t.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.signium.com/news/breaking-down-barriers-transforming-bureaucracy-into-agility/&quot;&gt;Research suggests&lt;/a&gt;
approval processes and org complexity delay innovation timelines by 30-50%.
&lt;a href=&quot;https://www.mckinsey.com/capabilities/mckinsey-digital/our-insights/tech-debt-reclaiming-tech-equity&quot;&gt;Technical debt may represent 40%&lt;/a&gt;
of tech estate in large enterprises.
Startups don’t have legacy architecture to protect.
Microsoft has to weigh every decision
against Azure, Office, Windows, and existing Copilot pricing baked into the codebase.&lt;/p&gt;

&lt;p&gt;The big-company playbook has largely been: “Add AI to the thing we already have.”&lt;/p&gt;

&lt;p&gt;That’s not nothing. Microsoft has
&lt;a href=&quot;https://techcrunch.com/2025/07/30/github-copilot-crosses-20-million-all-time-users/&quot;&gt;20 million GitHub Copilot users and 90% of Fortune 100 companies&lt;/a&gt;
as customers.
The distribution moat is real.
Whether it outweighs being months behind on features—that’s the key question,
and honestly it just might work.
I don’t see every colleague I know flocking to Claude Code right now&lt;sup id=&quot;fnref:flock-to-claude-code&quot;&gt;&lt;a href=&quot;#fn:flock-to-claude-code&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;,
and frankly it was pulling teeth to get folks to adopt the first wave of tools that came along.
(Some &lt;a href=&quot;https://www.hanselman.com/blog/dark-matter-developers-the-unseen-99&quot;&gt;dark matter developers&lt;/a&gt;
live under a rock.)&lt;/p&gt;

&lt;p&gt;So some folks may happily stay put for a while,
and wait till the next wave of agentic coding innovation is taken out of the terminal
and wrapped up very neatly in a productized bow for them.
(I’m not waiting.
There are outsized gains to be had now,
and the Claude Code burger tastes way too good.
Addictive, I might say; there’s extra salt and special sauce on this one, for sure.)&lt;/p&gt;

&lt;h2 id=&quot;theyre-all-burgers-&quot;&gt;They’re All Burgers 🍔&lt;/h2&gt;

&lt;p&gt;The copying isn’t slowing down.&lt;/p&gt;

&lt;p&gt;The next frontier is orchestration—factories of agents coordinating work across codebases.
Bleeding-edge &lt;a href=&quot;https://steve-yegge.medium.com/welcome-to-gas-town-4f25ee16dd04&quot;&gt;community approaches&lt;/a&gt; exist now
(bring your checkbook),
but the minute a big player ships something polished, everyone copies it.
Anthropic already borrows community ideas and brings them into Claude Code.
The first-party tasks feature is evidence:
the community invented persistent memory hacks for agents, and Anthropic productized it.&lt;/p&gt;

&lt;p&gt;Commoditization has never moved this fast. At some point, you’re just deciding: Quarter Pounder or Whopper?&lt;/p&gt;

&lt;p&gt;Once the market matures, the differences shrink.
The startups that scouted the territory proved it was real.
Product vision and execution count for a lot,
but if the execution is readily clone-able,
then you find yourself competing to some degree on distribution and brand loyalty.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.ideatovalue.com/inno/nickskillicorn/2022/04/first-mover-or-fast-follower-which-is-the-right-innovation-strategy-for-you/&quot;&gt;Golder and Tellis&lt;/a&gt; analyzed 500 companies across 50 product categories: 47% of first movers failed versus 8% of fast followers.
&lt;a href=&quot;https://steveblank.com/2010/10/04/why-pioneers-are-the-ones-with-the-arrows-in-their-backs/&quot;&gt;Steve Blank&lt;/a&gt;
put it bluntly:
“The jury is in. There’s no advantage to being first.
Astute fast-followers learn from first-movers by looking at the arrows in their backs.”&lt;/p&gt;

&lt;p&gt;So does that mean Microsoft wins in the end?
They have &lt;a href=&quot;https://survey.stackoverflow.co/2025/&quot;&gt;VS Code at 75.9% market share&lt;/a&gt;.
GitHub is where all the code lives.
They have enterprise procurement on their side.&lt;/p&gt;

&lt;p&gt;I used to work at a company that was a Microsoft shop, full stop.
They always chose the Microsoft option first; you only ventured outside the ecosystem with a compelling reason.
Some people only eat McDonald’s.&lt;sup id=&quot;fnref:amazon-fanbois&quot;&gt;&lt;a href=&quot;#fn:amazon-fanbois&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;But here’s what I think a lot of folks are missing: &lt;strong&gt;the capability matters more than the wrapper.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cursor can ship the slickest editor in the world
(they don’t),
but if Claude Code or Codex or whatever comes next is dramatically better,
it doesn’t matter whose IDE you’re using.
The intelligence is the product.
Everything else is a delivery vehicle.&lt;/p&gt;

&lt;p&gt;That’s why I think the real winners in this market aren’t the tool makers—they’re the model makers.&lt;sup id=&quot;fnref:google-advantage&quot;&gt;&lt;a href=&quot;#fn:google-advantage&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;
Cursor and GitHub Copilot are fighting over who gets to put the burger in your hand.
They don’t control the beef.&lt;sup id=&quot;fnref:wheres-the-beef&quot;&gt;&lt;a href=&quot;#fn:wheres-the-beef&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The burger wars didn’t end with one chain winning.
They ended with multiple billion-dollar franchises serving similar food to different customer segments.
That’s probably where we’re headed.&lt;/p&gt;

&lt;h2 id=&quot;the-sodium-hangover-&quot;&gt;The Sodium Hangover 🧂&lt;/h2&gt;

&lt;p&gt;So what do you actually do with this information?&lt;/p&gt;

&lt;p&gt;If you’re a developer: stop worrying about picking the “right” tool.
(But seriously, look at Claude Code, right now.)
They’re all converging.
Use whatever makes you productive today, switch when something better comes along,
and don’t build your identity around your editor.
The tool wars are a spectator sport.&lt;/p&gt;

&lt;p&gt;If you’re building a startup in this space: you’re in a foot race where the giants are many months behind you.
That’s your window.
Ship fast, accumulate learnings, build the things that can’t be copied in a week
(or even if they can, in a poor facsimile of what you did because you have the secret infra sauce).
And know that your real competition isn’t Microsoft—it’s the next startup that hasn’t launched yet,
and will find a new angle that builds on prior learnings.&lt;/p&gt;

&lt;p&gt;If you’re at a big company: maybe the honest move is to admit you’re playing Burger King’s game.
Drafting off good ideas isn’t shameful—it’s a strategy.
But your size is, as Nadella put it, a “massive disadvantage” when the underlying technology is shifting this fast.
You’re not going to out-innovate four people in a room who don’t have to schedule a meeting to make a decision.&lt;/p&gt;

&lt;p&gt;Cursor CEO &lt;a href=&quot;https://www.maginative.com/article/anysphere-raises-60m-for-ai-powered-coding-tool-cursor/&quot;&gt;Michael Truell&lt;/a&gt; articulated the startup bet:
“The goal is to replace coding with something much better.
Our aim is to build a magical tool that will one day write all the world’s software.”&lt;/p&gt;

&lt;p&gt;That’s not acquisition-speak. That’s someone who thinks they’re McDonald’s.&lt;/p&gt;

&lt;p&gt;And yet: competition has been unambiguously good for all of us.
&lt;a href=&quot;https://github.blog/news-insights/product-news/github-copilot-in-vscode-free/&quot;&gt;GitHub Copilot has a free tier now&lt;/a&gt;.
&lt;a href=&quot;https://aws.amazon.com/codewhisperer/&quot;&gt;Amazon CodeWhisperer&lt;/a&gt; is free for individuals.
Pricing pressure has driven Pro tiers down while features accelerate.&lt;sup id=&quot;fnref:chatgpt-ad-supported&quot;&gt;&lt;a href=&quot;#fn:chatgpt-ad-supported&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;8&lt;/a&gt;&lt;/sup&gt;
Developers capture the surplus.&lt;sup id=&quot;fnref:wardley-maps&quot;&gt;&lt;a href=&quot;#fn:wardley-maps&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The burger wars didn’t produce a winner. They produced an industry,
and gave cardiologists lots of work for the rest of time.&lt;/p&gt;

&lt;p&gt;Pick your burger. They’re all pretty good now.&lt;sup id=&quot;fnref:my-preferred-burger&quot;&gt;&lt;a href=&quot;#fn:my-preferred-burger&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;10&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:cursor-irony&quot;&gt;
      &lt;p&gt;There’s irony in that statement. Being a VS Code fork, Cursor will always be tracking upstream and stuck with an alternate extension marketplace. They also don’t own any models or have advantageous positions around them like the cloud providers do. That puts them in a precarious spot, IMO. But I digress! &lt;a href=&quot;#fnref:cursor-irony&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:merkle-tree-secret-sauce&quot;&gt;
      &lt;p&gt;Cursor’s secret sauce seemed to revolve around &lt;a href=&quot;https://cursor.com/blog/secure-codebase-indexing&quot;&gt;Merkle Trees&lt;/a&gt; as an efficient way to index your codebase, which was cool, but ultimately an approach that can be copied, and we’ve quickly trotted past with Claude Code; though Claude Code doesn’t do it directly built-in, it does have an extensive community and you can get code search going quickly with a &lt;a href=&quot;https://github.com/zilliztech/claude-context&quot;&gt;plugin&lt;/a&gt;. I’m certain Anthropic will do something native in the future for this, as they have all the means to do so. But also don’t discount the power of grep in a terminal setting—it’s powerful, and thus far I haven’t necessarily felt the need to have an index per se. &lt;a href=&quot;#fnref:merkle-tree-secret-sauce&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:aider-shame&quot;&gt;
      &lt;p&gt;I’m ashamed to say I had never heard of Aider before researching this post. &lt;a href=&quot;#fnref:aider-shame&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:flock-to-claude-code&quot;&gt;
      &lt;p&gt;Even though they should, since despite the fact that better things may come from well-known players, they’re losing valuable time that could have helped them learn how to wrangle agents in higher-order potentially GUI-ful tools in the future. &lt;a href=&quot;#fnref:flock-to-claude-code&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:amazon-fanbois&quot;&gt;
      &lt;p&gt;I’ve also seen the opposite: Amazon fanbois who have an unending, irrational hatred of Microsoft. Can’t we all just agree to hate Oracle? &lt;a href=&quot;#fnref:amazon-fanbois&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:google-advantage&quot;&gt;
      &lt;p&gt;And perhaps after that, cloud providers that have an advantageous position of hosting models for them and deals to use those models in their own products; Google is in a &lt;em&gt;very&lt;/em&gt; interesting position of having both of those things, and while I don’t think they have something rivaling Claude Code the tool, or Opus 4.5 the model, &lt;em&gt;yet&lt;/em&gt;, you know how this cat and mouse game goes. We’ll see! &lt;a href=&quot;#fnref:google-advantage&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:wheres-the-beef&quot;&gt;
      &lt;p&gt;And they need to ensure there is &lt;a href=&quot;https://www.youtube.com/watch?v=Ug75diEyiA0&quot;&gt;enough beef&lt;/a&gt; to begin with; anecdotally everyone has a CLI now like Claude Code, but some have pointed out to me how other CLI’s don’t have “the same taste” in terms of it providing as quality of output based on the techniques that are used. &lt;a href=&quot;#fnref:wheres-the-beef&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:chatgpt-ad-supported&quot;&gt;
      &lt;p&gt;All signs are pointing to you being able to use more features of ChatGPT free soon, &lt;a href=&quot;https://openai.com/index/our-approach-to-advertising-and-expanding-access/&quot;&gt;supported by ads&lt;/a&gt;—one of the final stops of commoditization in various markets, having services driven to “Free,” means inevitably &lt;em&gt;you&lt;/em&gt; become the product. So don’t be surprised if Mickey D’s shows up inline in your ChatGPT response touting their newest scientific advances in the Egg McMuffin. (And now this blog post has truly come full circle.) &lt;a href=&quot;#fnref:chatgpt-ad-supported&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:wardley-maps&quot;&gt;
      &lt;p&gt;Go read &lt;a href=&quot;https://medium.com/wardleymaps&quot;&gt;Wardley Maps&lt;/a&gt; if you want to understand the forces behind why commoditization comes for everything eventually. &lt;a href=&quot;#fnref:wardley-maps&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:my-preferred-burger&quot;&gt;
      &lt;p&gt;I prefer the &lt;a href=&quot;https://www.burgerking.ee/en/menu/flame-grilled-burgers/bacon-king/&quot;&gt;Bacon King&lt;/a&gt;, and my current Bacon King is Claude Code. But I also like some modifiers. No ketchup, light mayo, add light mustard, add pickles. (Fun fact: I have a former manager who cannot tolerate anything in the cucumber realm—hello, Augusto—in almost violent ways where he’d flip over the restaurant table and walk out if you serve him this particular strain of gourd. We’ve had to restrain him several times.) I’m currently working on modifying Claude Code to my liking, and figuring out if I want regular fries or chicken fries (flavors of orchestration) and my choice of dipping sauce, probably ranch (my personal taste injected into these tools). I wish Burger King had caffeine free Diet Dr Pepper, then everything would be perfect. Where did Dr Pepper get his PhD from? Ok this ADHD footnote is getting weird. Bye! &lt;a href=&quot;#fnref:my-preferred-burger&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/02/mcdonalds-burger-king-and-the-innovators-dilemma/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/02/mcdonalds-burger-king-and-the-innovators-dilemma/</guid>
        
        <category>ai</category>
        
        <category>coding</category>
        
        
      </item>
    
      <item>
        <title>Tidying Your Home for Your AI Guests</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>I want you to imagine the place that you live in.
</description>
        <content:encoded>&lt;p&gt;I want you to imagine the place that you live in.&lt;/p&gt;

&lt;p&gt;Are your papers in a tidy pile, with the ones you want to keep cleanly filed in some kind of filing cabinet?&lt;/p&gt;

&lt;p&gt;Are your books scattered throughout the house on disparate shelves?&lt;/p&gt;

&lt;p&gt;Are your clothes organized with a certain strategy (by type, by season)… or is it a free-for-all?
(I’m going to guess at minimum you have a sock drawer.)&lt;/p&gt;

&lt;p&gt;At our last place, we had the hand drill sitting in the coat closet,
with other tools scattered in different places throughout the house…
so trust me, we are not saints in this regard.&lt;/p&gt;

&lt;p&gt;Now imagine if you invited someone into your house and asked them to categorize your belongings,
or even just figure out how to live and take care of things in your place;
maybe you’re starting an Airbnb!&lt;/p&gt;

&lt;p&gt;If the frying pan is in the upstairs bedroom closet, then people will have a hard time finding it…&lt;/p&gt;

&lt;p&gt;This is not an advocacy post for pursuing a &lt;a href=&quot;https://konmari.com/&quot;&gt;Marie Kondo&lt;/a&gt; level of tidiness at your house
(although shout out to my awesome mentor
whose &lt;a href=&quot;https://www.amazon.com/Spark-Joy-Illustrated-Organizing-Changing/dp/1607749726/&quot;&gt;books&lt;/a&gt;
finally taught me as a full-grown human how to
&lt;a href=&quot;https://www.amazon.com/Life-Changing-Magic-Tidying-Decluttering-Organizing/dp/1607747308/&quot;&gt;fold clothes&lt;/a&gt; properly).&lt;/p&gt;

&lt;p&gt;This drawn-out analogy gets us to this point:&lt;/p&gt;

&lt;p&gt;The way you work in an enterprise is likely much like an untidy house,
and trying to bring AI into the mix to provide value atop that untidiness is going to have challenges.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/robots-untidy-data-house.png&quot; alt=&quot;A digital illustration of a cluttered living room with piles of papers, books on crowded shelves, and tools scattered on the floor. Two friendly, futuristic AI robots stand just inside the open front door holding suitcases, looking into the disorganized space as sunlight streams in from outside, suggesting AI arriving as a guest in an untidy home.&quot; /&gt;
&lt;em&gt;“&lt;a href=&quot;https://en.wikipedia.org/wiki/All_your_base_are_belong_to_us&quot;&gt;All your base are belong to us&lt;/a&gt;—OMG!”&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;living-with-the-clutter&quot;&gt;Living With the Clutter&lt;/h2&gt;

&lt;p&gt;I’ve seen many scenarios over the last few years where there is a desire to implement AI to solve some type of problem,
but an unwillingness to address any of the data untidiness,
or further, the systems or ways of working that grew up around it.&lt;/p&gt;

&lt;p&gt;A quick note on “tidy data”:
there’s a &lt;a href=&quot;https://www.jstatsoft.org/article/view/v059i10&quot;&gt;definition&lt;/a&gt;
for &lt;a href=&quot;https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html&quot;&gt;tidy data&lt;/a&gt;
that revolves around rows and columns,
which isn’t &lt;em&gt;necessarily&lt;/em&gt; what I’m referring to here
(although it is a great principle to abide by for structured data).&lt;/p&gt;

&lt;p&gt;What I mean is how datasets and documents and artifacts are organized among systems,
and within systems,
and especially the boundaries we create around organization and access.&lt;/p&gt;

&lt;p&gt;Maybe you have duplicative tools that different folks are using to store similar kinds of data.&lt;/p&gt;

&lt;p&gt;Maybe you are all within the same tool,
but have organized your data along boundaries in a way that is not conducive to consumption,
either by AI, and honestly, sometimes by end users, too.&lt;/p&gt;

&lt;p&gt;Some business requirements and processes around how data is created and used are hard and fast and unmovable.&lt;/p&gt;

&lt;p&gt;But many are not, and could be re-oriented to be not only more friendly for AI,
but also more friendly for the people operating within these environments.&lt;/p&gt;

&lt;p&gt;This is where things get expensive in a very predictable way…&lt;/p&gt;

&lt;p&gt;I’ve encountered several concrete examples where there was a desire to set up an AI RAG approach as a “band-aid”
to accommodate the scattered nature of unstructured data across various systems (and within those systems)
when, in actuality, a “house tidying” effort would have more readily allowed
for using AI tools—some of which are off-the-shelf and readily available—to accommodate the desired outcomes.&lt;/p&gt;

&lt;p&gt;The intention is not to dunk on RAG (retrieval augmented generation),
but it becomes telling when the primary purpose for rolling up your sleeves
and taking the time to populate a vector database is:&lt;/p&gt;

&lt;p&gt;“We don’t want to change anything about how we work… just make the AI deal with it.”&lt;/p&gt;

&lt;p&gt;In one case I saw, there was a desire to provide RAG
for data that people &lt;em&gt;didn’t have access to&lt;/em&gt; in the source systems…
but could allow for folks request access “on demand”
when linked to the source through an LLM answer backed by retrieved data.&lt;/p&gt;

&lt;p&gt;The interesting thing about this scenario was: the data was &lt;em&gt;not sensitive&lt;/em&gt;; access could have been easily granted,
or the data could have been reorganized so it lived in one place, with boundaries aligned to the use case.&lt;/p&gt;

&lt;p&gt;But the ask was catered around a desire to not change the way that people were currently working.&lt;/p&gt;

&lt;p&gt;If we took a moment to reorganize the data so people did have access to it,
not only would it have been an improved way of working that benefited everyone,
but AI—and particularly in this case, existing off-the-shelf AI capabilities—could
have accommodated the approach with far less bespoke infrastructure.&lt;/p&gt;

&lt;h2 id=&quot;a-bigger-house-isnt-a-cleaner-one&quot;&gt;A Bigger House Isn’t a Cleaner One&lt;/h2&gt;

&lt;p&gt;One might argue at this point: is this not where a data platform comes in to save the day?&lt;/p&gt;

&lt;p&gt;Sometimes, yes.&lt;/p&gt;

&lt;p&gt;But the issue with that thought process is that it often just shifts the problem:
You still have to decide what to ingest,
concern yourself with security trimming from the source,
and you may still have to tidy and organize the data when things hit the platform.&lt;/p&gt;

&lt;p&gt;Also, if the current ways of working lend themselves to new sources of data springing up all the time,
you’re going to be constantly doing repeat trips to the folks responsible for ingestion…
or you may decide to ingest every new source that appears blindly and figure it out later,
which can get costly and unwieldy and unapproachable very quickly.&lt;/p&gt;

&lt;p&gt;(That is, unless you have ascended to the most elite levels
of &lt;a href=&quot;https://www.datamesh-architecture.com/&quot;&gt;Data Mesh&lt;/a&gt; implementation,
at a maturity level where a business user can simply click a button to onboard a new data source to your data platform
for downstream consumption;
if this is you, you are in an enviable position in the corporate landscape,
and I’d like to have a chat with you.)&lt;/p&gt;

&lt;p&gt;This is analogous to adding on a new room or wing to your home,
and having to decide what from the various piles should go there…&lt;/p&gt;

&lt;p&gt;You aren’t really avoiding the mess.&lt;/p&gt;

&lt;p&gt;You’re sifting through it.&lt;/p&gt;

&lt;h2 id=&quot;the-life-changing-magic-of-tidying-up&quot;&gt;The Life-Changing Magic of Tidying Up&lt;/h2&gt;

&lt;p&gt;Can you implement AI with untidy data spread across disparate systems?&lt;/p&gt;

&lt;p&gt;Yes, you can, but it winds up being vastly more expensive to implement.&lt;/p&gt;

&lt;p&gt;Worse, it can lock you into a way of working that you didn’t actually want to cement for the next several years.&lt;/p&gt;

&lt;p&gt;Don’t let AI become the reason you never address any of your problematic processes or data organization.&lt;/p&gt;

&lt;p&gt;In fact, AI is the perfect opportunity to take a long, hard look at your systems and tools landscape
and decide if this is how you want your AI guests to see your house.&lt;/p&gt;

&lt;p&gt;Also: you’re going to have multiple AI house guests over time…
the ones that are here today will be totally different in the future.&lt;/p&gt;

&lt;p&gt;The costs of not dealing with untidy data and processes now
are only going to be repeatedly borne out and compound over time
when you decide you want to switch models, tools, vendors, or strategies later.&lt;/p&gt;

&lt;p&gt;Tidy the house.&lt;/p&gt;

&lt;p&gt;Then, &lt;a href=&quot;https://www.bbc.com/news/articles/clyg63e3mq4o&quot;&gt;invite the robots in&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
        <pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2026/01/tidying-your-home-for-your-ai-guests/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2026/01/tidying-your-home-for-your-ai-guests/</guid>
        
        <category>ai</category>
        
        <category>data</category>
        
        
      </item>
    
      <item>
        <title>Is It Safe to Write a Blog Post That Is Not About AI?</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>I have a confession to make...
</description>
        <content:encoded>&lt;p&gt;&lt;em&gt;Warning: Occasional pithy humor and light-hearted sarcasm ahead.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have a confession to make…&lt;/p&gt;

&lt;p&gt;Lately, I’ve been thinking about writing a blog post that is not about AI.&lt;/p&gt;

&lt;p&gt;Is it safe to do this?&lt;/p&gt;

&lt;p&gt;Or will it pull me further away from the Light?&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/Michelangelo_-_Creation_of_Adam_(cropped)_meme.jpg&quot; alt=&quot;A cropped version of Michelangelo&apos;s The Creation of Adam. On the right, God reaches out from a cluster of figures, labeled &amp;quot;AI&amp;quot; in bold white Impact-style text. On the left, Adam reclines on the ground, reaching back, labeled &amp;quot;THE REST OF TECHNOLOGY.&amp;quot; The two hands nearly touch, parodying the original scene to suggest AI as a dominant, godlike force eclipsing all other technology.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-debate-about-the-possible&quot;&gt;The Debate About the Possible&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;“When a distinguished but elderly scientist states that something is possible, he is almost certainly right.&lt;/p&gt;

  &lt;p&gt;When he states that something is impossible, he is very probably wrong.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Arthur C. Clarke’s &lt;a href=&quot;https://en.wikipedia.org/wiki/Clarke%27s_three_laws&quot;&gt;First Law&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I often let colleagues know that I don’t have a great photographic memory for verbal conversations
(and therefore tools like Teams meetings with recordings, transcripts, AI recaps
and the ability to ask questions retroactively about the meeting
are an absolute game-changer and life-saver for me).&lt;/p&gt;

&lt;p&gt;My lack of great conversational photographic memory is definitely a “me” thing,
because I have other colleagues who have an almost spooky ability
to remember every single aspect of what was said in a conversation—in many ways,
those types of folks are their own personal Copilot
(whereas for folks like me, I very much need to lean on note-taking and tools and systems to help me stay organized).&lt;/p&gt;

&lt;p&gt;I am telling you this because the conversations I &lt;em&gt;do&lt;/em&gt; tend to remember the most
are ones tied to emotions that lodge themselves into my amygdala.&lt;/p&gt;

&lt;p&gt;And because of that, I have a vivid recollection of what I was thinking and how I felt in 2023.&lt;/p&gt;

&lt;p&gt;When ChatGPT became popular, the public discourse was starting to rage about what LLM-based AI could and couldn’t do.&lt;/p&gt;

&lt;p&gt;Further, people began delving into what AI would be able to do in the future
and what would remain in the realm of “Sci-Fi” (at least, in our lifetimes).&lt;/p&gt;

&lt;p&gt;2023, frankly, was a bit of a mentally scary time for critical thinkers,
because any hint of skepticism about what AI could or couldn’t do was often met with the retort:&lt;/p&gt;

&lt;p&gt;“Well, it’s not possible &lt;em&gt;yet&lt;/em&gt;.”&lt;/p&gt;

&lt;p&gt;This dead end line of discussion, I felt, wasn’t helpful,
and further the pushed notion that AI would solve ‘X’—where
‘X’ was, in many cases, an already solved problem with existing technology—often
dismissing all the years of innovation and approaches
that have been around solving problems for people for many, many years;
the humble IF/ELSE deterministic logic has propelled us as a society forward
and shouldn’t be tossed out with the proverbial bath water.&lt;/p&gt;

&lt;h2 id=&quot;what-has-been-possible-for-a-long-time&quot;&gt;What Has Been Possible for a Long Time&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2001-video-call.jpg&quot; alt=&quot;A meme-style still from 2001: A Space Odyssey shows a man seated at a futuristic video console, smiling as he speaks to a child on a screen. Overlaid text on the left reads, &amp;quot;CAN&apos;T YOU THINK OF ANYTHING ELSE YOU WANT FOR YOUR BIRTHDAY? SOMETHING VERY SPECIAL?&amp;quot; Overlaid text on the right, next to the child&apos;s image, reads, &amp;quot;SOMETHING THAT OUTPERFORMS HUMANS ON COMPLEX REASONING BENCHMARKS.&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The only way of discovering the limits of the possible is to venture a little way past them into the impossible.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Arthur C. Clarke’s &lt;a href=&quot;https://en.wikipedia.org/wiki/Clarke%27s_three_laws&quot;&gt;Second Law&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Machine learning is &lt;a href=&quot;https://en.wikipedia.org/wiki/Machine_learning&quot;&gt;not new&lt;/a&gt;,
and has existed in some shape or form for many decades,
with papers going back to the &lt;a href=&quot;https://en.wikipedia.org/wiki/Perceptron&quot;&gt;1940s&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Further, computer science and software engineering are not new,
and have been deployed for many years in many facets of our existence to help solve real problems
and (hopefully, but not always) make our lives better and improve the human condition.&lt;/p&gt;

&lt;p&gt;But it always begets the question of what else is possible given the technology we have now,
and will have in the future.&lt;/p&gt;

&lt;p&gt;In 2025, I’ve seen enough of AI the past couple of years to form some opinions.&lt;/p&gt;

&lt;p&gt;And so have others:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“I see no line of sight into AI completely replacing programmers.”&lt;/p&gt;

  &lt;p&gt;- &lt;a href=&quot;https://youtu.be/PjInF4wbmxM?si=Ampik7G3IV3V0naA&amp;amp;t=1231&quot;&gt;Mark Russinovich&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is ironic that the relatively mild and reserved “hot takes” about AI technology
have become the “provocative” or “controversial” ones.&lt;/p&gt;

&lt;p&gt;I would rather be confident in my (rather mild and grounded) opinions and be wrong,
and change my mind in the future,
than to forever remain waffle-y about what is possible (or not possible).&lt;/p&gt;

&lt;p&gt;I also believe, like Mark, that &lt;a href=&quot;https://youtu.be/7FhoUAidsFg?si=GtiAZejmK8c0IwkC&quot;&gt;hope is not a strategy&lt;/a&gt;,
and I am not going to make decisions based on the pure dream that “One day, AI will handle all of this.”&lt;/p&gt;

&lt;p&gt;In addition to that, we already have &lt;em&gt;so much existing technology&lt;/em&gt; today
that has made what used to be considered “the impossible,” possible.&lt;/p&gt;

&lt;p&gt;However most of that progress happens by venturing a &lt;em&gt;little&lt;/em&gt; bit beyond today’s limits;
in stark contrast, we have folks engaging in wild speculation about what &lt;em&gt;could&lt;/em&gt; happen &lt;em&gt;way&lt;/em&gt; beyond today’s limits,
not just about what is possible, but also on what timeframes, and in what ways.&lt;/p&gt;

&lt;h2 id=&quot;an-alien-from-outer-space-&quot;&gt;An Alien From Outer Space 👽&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2001-a-Space-Odyssey-apes-monolith.jpg&quot; alt=&quot;A meme-style image inspired by 2001: A Space Odyssey: a group of early hominids stand and crouch among rocky terrain at sunrise, surrounding a tall black monolith in the center. Overlaid white Impact-style text reads at the top, &amp;quot;SOON YOU WON&apos;T BE MANAGING GATHERERS,&amp;quot;&amp;quot; and at the bottom, &amp;quot;YOU&apos;LL BE MANAGING AGENTS—ER, I MEAN HUNTERS,&amp;quot;&amp;quot; humorously comparing modern AI agent hype to prehistoric evolution.&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Any sufficiently advanced technology is indistinguishable from magic.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;Arthur C. Clarke’s &lt;a href=&quot;https://en.wikipedia.org/wiki/Clarke%27s_three_laws&quot;&gt;Third Law&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This debate about the possible versus the impossible happens in many corners of the internet,
but typically many technical folks who delve into aspects of AI tend to engage in conversations the most intensely.&lt;/p&gt;

&lt;p&gt;Which begs the question:
Where does this leave the rest of humanity that may not be technical,
and for whom AI is a totally foreign entity?&lt;/p&gt;

&lt;p&gt;Many are now trying to (or perhaps, due to fear, in many cases, consciously or unconsciously, trying &lt;em&gt;not&lt;/em&gt; to)
wrap their heads around this brand new “thing.”&lt;/p&gt;

&lt;p&gt;Folks often liken the arrival of modern LLM-based AI to the creation of something analogous to HAL 9000
from the movie &lt;em&gt;2001: A Space Odyssey&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;However, I often wonder if the arrival of AI is less like HAL,
and more like the Monolith that appears in multiple parts of that movie.&lt;/p&gt;

&lt;p&gt;In many ways it feels like an alien technology showed up in our neck of the woods in this Milky Way galaxy,
and everyone is trying to wrap their heads around it.&lt;/p&gt;

&lt;p&gt;Another unfortunate byproduct of this is, because AI may feel like “magic” to many people,
they pin all of their hopes and dreams onto the technology, and &lt;em&gt;hallucinate&lt;/em&gt; things that AI can and can’t do.&lt;/p&gt;

&lt;p&gt;The problem is that these human hallucinations aren’t just incorrect—they’re expensive.
They turn into roadmaps, budgets, and organizational decisions.&lt;/p&gt;

&lt;p&gt;And that’s when the “magic” stops being fun,
and often turns into a CFO discussion of “What ROI and/or value are we getting out of this?”&lt;/p&gt;

&lt;p&gt;And it’s not because the technology doesn’t have potential,
but rather because it was deployed in a “wishful thinking” way that dead-ended
either due to very real data or technical or even economical constraints,
or perhaps simply due to the fact that it didn’t solve real problems for real people,
or a myriad of other potential issues.
(This is yet another entire rabbit hole suitable for another blog post.)&lt;/p&gt;

&lt;p&gt;Maybe the alien life form not only gave us something perceived as “magic,”
but perhaps by extension we contracted a disease from it in the form of “AI Rabies,”
where we are metaphorically foaming at the mouth about what is possible
and losing our sense of rationality and once again, &lt;em&gt;hallucinate&lt;/em&gt; about the possible,
or barring that just lose our common sense about what can be helpful to meet real user needs.&lt;/p&gt;

&lt;p&gt;Deployed use case issues aside, this is also how you get to one of the weirdest parts of this moment:
not the technology itself, but the mythology we’re building around it.&lt;/p&gt;

&lt;p&gt;Which is why I flinch a little bit every time I see a certain phrase making the rounds…&lt;/p&gt;

&lt;h2 id=&quot;beyond-the-infinite-️&quot;&gt;Beyond the Infinite ♾️&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/2001-ai-meme.jpg&quot; alt=&quot;A blurred, dramatic close-up of a wide-eyed astronaut inside a helmet, lit by reflections and glowing lights, with bold white Impact-style text across the center reading, &amp;quot;THIS IS THE WORST AI IS EVER GOING TO BE.&amp;quot;&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“This is the worst AI is ever going to be.”&lt;/p&gt;

  &lt;p&gt;- &lt;em&gt;A trite saying &lt;a href=&quot;https://www.google.com/search?q=this+is+the+worst+ai+is+ever+going+to+be&amp;amp;oq=this+is+the+worst+ai+is+ever+going+to+be&amp;amp;gs_lcrp=EgRlZGdlKgYIABBFGDkyBggAEEUYOTIHCAEQ6wcYQNIBCDU4MzZqMGoxqAIAsAIA&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8&quot;&gt;floating around the internet&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Dissecting that annoying line could fill up a whole additional blog post.
(Is it just the models getting better? And purely through more data?
Or is it new type of model techniques?
Or is it techniques we use &lt;em&gt;around&lt;/em&gt; the models that get better?
&lt;a href=&quot;https://giphy.com/gifs/sonypictures-jennifer-lawrence-hot-ones-no-hard-feelings-h58E0JsuK3h3d8B1do&quot;&gt;What do you mean?!&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This utterly useless phrase basically amounts to: “Things improve over time.”&lt;/p&gt;

&lt;p&gt;Wow.&lt;/p&gt;

&lt;p&gt;Beyond the Infinite(ly) stupid.&lt;/p&gt;

&lt;p&gt;Everything tends to improve over time, not just AI…&lt;/p&gt;

&lt;p&gt;The humble washing machine has improved over time.&lt;/p&gt;

&lt;p&gt;I could cite a ton of additional mundane examples here, but you get the idea…&lt;/p&gt;

&lt;p&gt;Bringing it back to an example that is less mundane:
Models will improve over time, and model layers and weights are one thing,
but few appreciate the immense amount of work that goes into model &lt;em&gt;serving&lt;/em&gt; technologies
like Ray Serve and other libraries that make models like LLMs &lt;em&gt;actually&lt;/em&gt; possible to use,
and improvements &lt;em&gt;will&lt;/em&gt; be realized in the surrounding technologies
that are all absolutely essential to making AI possible to use,
but may not get their full due, nor the meaningful amount of mind share to discuss openly.&lt;/p&gt;

&lt;p&gt;But the most insidious thing about this idiotic phrase is
the simultaneous sense of wonder and awe and existential dread it instills in people
while we are all collectively barreling through the psychological daily onslaught of new AI advancements,
and it is completely uncalled for and unnecessary…&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;If it is truly the worst it is ever going to be, I still at this time find AI immensely useful.&lt;/p&gt;

&lt;p&gt;But to find AI useful does not automatically negate the utility of everything else in the technology world
that has come before it, and is still alive and well and being used today.&lt;/p&gt;

&lt;p&gt;My coffee maker at home is useful.&lt;/p&gt;

&lt;p&gt;This blog post that I am writing right now
and host on GitHub Pages with Cloudflare in front of it is (hopefully) useful,
and fueled by 100% organic pure human thought and creativity—the
only AI help for this post came from creating alt text for the images, to improve accessibility
(which is a great use case for AI).&lt;/p&gt;

&lt;p&gt;My &lt;a href=&quot;https://github.com/ryanspletzer/dev-machine-setup&quot;&gt;dev machine setup&lt;/a&gt; scripts are useful
in saving days setting up a machine by hand,
both for me and for others I know that are using them.
(AI helped me write the latest versions of those,
but AI didn’t instill the philosophy of design
nor the essence of simplicity I wanted to achieve in the latest iterations of them,
nor did it provide the years of learning and the acquired taste of what “good” looks like to hone my approach,
nor does it perform the actual setup.)&lt;/p&gt;

&lt;p&gt;And in consideration of the time span of all these decades of useful technologies that we’ve accumulated,
and will continue to accumulate independently of AI,
&lt;em&gt;we need to be able to have open ways to talk about them&lt;/em&gt;,
and not have those discussions be dismissed out of hand because they are somehow “boring” and not aligned
with the prevailing AI narrative.&lt;/p&gt;

&lt;p&gt;For example, to get MCP to work properly,
you have to build up an understanding
of a &lt;a href=&quot;https://github.com/ryanspletzer/oidc-oauth-spec-graph/blob/main/graph.md&quot;&gt;web of interrelated IETF specifications around OAuth and adjacent technologies&lt;/a&gt;,
which to some may feel “boring,” but for us engineers,
is essential to produce something that is not a walking security hazard with no auth (or dubious auth)
that vibe coded its way out of its containment zone of Lovable or Replit.&lt;/p&gt;

&lt;p&gt;In fact, most of the time I’ve spent at work with our teams &lt;em&gt;delivering&lt;/em&gt; AI for the enterprise involves using
technologies and techniques around data engineering and data science and automation and full-stack software engineering
and cloud infrastructure (and more) that &lt;em&gt;have nothing to do with AI whatsoever&lt;/em&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Thankfully, I don’t think I will be smote by a bolt of lightning
if I write a future blog post that has nothing to do with AI.&lt;/p&gt;

&lt;p&gt;(And I won’t be smote for this one because by my count there are 38 mentions of the word “AI” in this blog post,
including the one in this sentence—but please count for yourself, because I am a human that can make mistakes!)&lt;/p&gt;

&lt;p&gt;AI didn’t create our current set of technology—rather, our existing technology helped us create AI.&lt;/p&gt;

&lt;p&gt;The introduction of AI is additive, not subtractive, to the existing technology we have.&lt;/p&gt;

&lt;p&gt;And, coupled with existing technology, AI is more than likely going to help us do amazing things in the future.&lt;/p&gt;

&lt;p&gt;With this in mind, maybe, just maybe, the mental model of how we got here, and where we’re going,
needs to be thought about a little bit differently,
and should allow more space for more discussion about all types of technology, not just AI.&lt;/p&gt;

&lt;p&gt;And perhaps we can try to take some things a little less seriously and remember to have a
&lt;a href=&quot;https://www.ocregister.com/wp-content/uploads/migration/mnb/mnbih3-b781115628z.120130524122745000ga11e4tlo.3.jpg?w=620&quot;&gt;little bit&lt;/a&gt;
of &lt;a href=&quot;https://www.youtube.com/watch?v=F9-zi-qKg8I&quot;&gt;fun&lt;/a&gt; along the way, too—so in the spirit of fun,
let’s flip this script:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/Michelangelo_-_Creation_of_Adam_(cropped)_meme_reversed.jpg&quot; alt=&quot;A meme-style reinterpretation of Michelangelo&apos;s The Creation of Adam. On the left, Adam reclines with the label &amp;quot;AI&amp;quot; in bold white text. On the right, God reaches out from a cloud of figures labeled &amp;quot;THE REST OF TECHNOLOGY.&amp;quot; Their outstretched hands nearly touch, parodying the original composition to suggest AI is not above the rest of technology.&quot; /&gt;&lt;/p&gt;
</content:encoded>
        <pubDate>Sat, 13 Dec 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/12/is-it-safe-to-write-a-blog-post-that-is-not-about-ai/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/12/is-it-safe-to-write-a-blog-post-that-is-not-about-ai/</guid>
        
        <category>ai</category>
        
        <category>technology</category>
        
        
      </item>
    
      <item>
        <title>Visualizing the OAuth &amp; OpenID Connect Spec Graph</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>I created an OpenID Connect and OAuth spec graph in a Mermaid diagram in a GitHub repository.
</description>
        <content:encoded>&lt;p&gt;I have a solid grasp of OAuth 2.0 and OpenID Connect (OIDC) from past projects,
notably from going heads-down and reading the core specs for those protocols
to implement a federated identity provider with open-source frameworks. (It was a doozy.)&lt;/p&gt;

&lt;p&gt;But then along came the Model Context Protocol (MCP)—an emerging standard for AI agents
that leans on OAuth 2.1 and various OAuth extensions.&lt;/p&gt;

&lt;p&gt;Suddenly I was up to my eyeballs in specs again: dynamic client registration, PKCE, you name it.
Each spec I opened would reference two more that I “should” go read.&lt;/p&gt;

&lt;p&gt;Before long, I had a cascade of browser tabs open
and was struggling to keep track of how all these documents fit together.&lt;/p&gt;

&lt;p&gt;If you’ve ever tried to implement OIDC/OAuth, you probably know the feeling—a specification sprawl
where every RFC and spec points to another,
and you’re left juggling a dozen tabs in search of the full picture.&lt;/p&gt;

&lt;p&gt;In my case, the trigger was MCP.
It adopts OAuth 2.1 for authorization, which meant revisiting the OAuth core spec (RFC 6749) and many related ones.
Reading the MCP spec led me to open the OAuth Dynamic Client Registration spec (RFC 7591),
which led me to the OAuth 2.0 Authorization Server Metadata spec (RFC 8414),
which led me back to OIDC Discovery… you get the picture.&lt;/p&gt;

&lt;p&gt;Even experienced developers can find this overwhelming—it’s easy to lose track of which specs are fundamental,
which are optional extensions, and where to even begin.&lt;/p&gt;

&lt;p&gt;Trying to navigate the OAuth/OIDC ecosystem through official specs alone often results in “browser tab explosion.”
For example, you start with the OpenID Connect Core spec,
which tells you it’s built on OAuth 2.0 (so off you go to read RFC 6749).
OAuth 2.0 in turn mentions bearer tokens (RFC 6750), so open that too.
OIDC also relies on JSON Web Tokens (JWT, RFC 7519),
which themselves rely on JOSE specs like JWS, JWE, and JWK—there go three more tabs.&lt;/p&gt;

&lt;p&gt;Pretty soon you’ve got 10+ spec documents open side by side,
and you’re doing a lot of mental context-switching to piece together how they all relate.&lt;/p&gt;

&lt;p&gt;This spec overload isn’t just an academic problem; it hits practical questions:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Understanding dependencies:&lt;/strong&gt; Which specs build on which others?
(e.g. does implementing feature X require reading another spec?)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Finding the right document:&lt;/strong&gt; If you need to implement Proof Key for Code Exchange (PKCE),
do you read the OAuth 2.0 core spec, an extension RFC, or something in OIDC?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Not missing anything important:&lt;/strong&gt; Are there security best-practice specs (like OAuth 2.0 Security BCP) or
extension drafts that you should be aware of?&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Teaching or documentation:&lt;/strong&gt; How do you explain this tangle of specs to someone else
without their eyes glazing over?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the OAuth/OIDC standard family is rich but complex, and it’s easy to get lost in the cross-references.
I personally hit a point where I knew there had to be a better way to see “the big picture”
than control-tabbing between RFCs.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/oidc-oauth-spec-graph.png&quot; alt=&quot;A dark-themed diagram titled “OpenID Connect and OAuth Specification Graph,” showing how OAuth 2.0, OpenID Connect, and JOSE/JWT specifications connect through references. Boxes for each RFC (e.g., 6749, 7519, 9068) are grouped by category and linked with lines to illustrate their relationships.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-solution-a-visual-map-of-the-specs&quot;&gt;The Solution: A Visual Map of the Specs&lt;/h2&gt;

&lt;p&gt;To tackle this, I decided to map out the spec ecosystem
&lt;a href=&quot;https://github.com/ryanspletzer/oidc-oauth-spec-graph/blob/main/graph.md&quot;&gt;visually&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of manually drawing boxes and arrows (and inevitably forgetting some),
I leveraged AI coding assistance to help build a Mermaid diagram of the specifications
and their references to each other.&lt;/p&gt;

&lt;p&gt;Mermaid lets you write text-based diagrams, and GitHub can render them natively.
I fed my assistant (think ChatGPT/Copilot) an initial list of the OAuth, OIDC, and JWT specs,
and asked it to help generate an initial graph definition,
then continued to add more ancillary specs from there.&lt;/p&gt;

&lt;p&gt;The result is an “OpenID Connect and OAuth Specification Graph”
that now lives in a &lt;a href=&quot;https://github.com/ryanspletzer/oidc-oauth-spec-graph&quot;&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This visual graph shows most of the major specifications in the OAuth/OIDC universe
and draws arrows to represent their reference relationships.
I grouped the nodes by category to make it clearer:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OAuth 2.0 Core &amp;amp; Extensions&lt;/strong&gt; – the base framework (RFC 6749) and its many RFC extensions
(PKCE, token revocation, device flow, etc.).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JOSE/JWT specs&lt;/strong&gt; – the JSON Web Signature/Encryption/Key standards and JWT itself,
which provide the cryptographic backbone for tokens.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OpenID Connect&lt;/strong&gt; – the identity layer specs (OIDC Core, Discovery, Dynamic Client Registration)
built on top of OAuth 2.0.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UMA (User-Managed Access)&lt;/strong&gt; – an OAuth-based protocol for user-controlled resource sharing.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Miscellaneous/Modern&lt;/strong&gt; – newer drafts and best practices (OAuth 2.1, Security BCP, DPoP, etc.)
that integrate or update the above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Crucially, each node in the graph is clickable, linking directly to the official spec document or RFC text.&lt;/p&gt;

&lt;p&gt;By visualizing the web of specs, this fun weekend project aims to turn that intimidating tangle into a navigable map.&lt;/p&gt;

&lt;p&gt;For me, this has already been a huge help in regaining context quickly when diving back into OAuth land
to read up on the dynamic client registration and other specs referenced by MCP,
after having read the core OIDC and OAuth and JWT specs many years ago…&lt;/p&gt;

&lt;h2 id=&quot;building-the-spec-graph-with-a-little-ai-help&quot;&gt;Building the Spec Graph (with a Little AI Help)&lt;/h2&gt;

&lt;p&gt;One fun aspect of this project was using AI pair-programming to build the graph.&lt;/p&gt;

&lt;p&gt;Hand-coding a Mermaid diagram with ~50 nodes and dozens of arrows would be tedious (and error-prone) to do solo.&lt;/p&gt;

&lt;p&gt;Instead, I iteratively prompted a coding assistant to help generate the Mermaid markup.&lt;/p&gt;

&lt;p&gt;Using AI in this way felt like having a knowledgeable co-author:
it could recall obscure RFC numbers and the relationships mentioned in their text,
which saved me a ton of manual cross-checking.&lt;/p&gt;

&lt;p&gt;This was a great reminder that LLMs didn’t make understanding specs obsolete—but
they did make assembling reference materials faster.
Instead of manually collating references from dozens of documents,
I could focus on validating and fine-tuning the output.&lt;/p&gt;

&lt;p&gt;The AI assistance turned a daunting documentation task into something almost fun.&lt;/p&gt;

&lt;h2 id=&quot;mapping-the-oauthoidc-ecosystem-key-relationships&quot;&gt;Mapping the OAuth/OIDC Ecosystem: Key Relationships&lt;/h2&gt;

&lt;p&gt;So what does our spec graph reveal?&lt;/p&gt;

&lt;p&gt;At a high level, it confirms the layered architecture of modern auth standards.
Here are some of the key relationships visualized:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;OAuth 2.0 is the foundation&lt;/strong&gt; – Nearly every other spec extends or references
the core OAuth 2.0 framework (RFC 6749).
If you’re dealing with tokens or authorization flows, OAuth2 is the base context.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JOSE is the cryptographic foundation&lt;/strong&gt; – JSON Web Signature, Encryption, Keys, etc., and JWT
all provide the security underpinnings.
Many higher-level specs depend on these for signing/encrypting tokens.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OpenID Connect builds on OAuth + JWT&lt;/strong&gt; – OIDC uses OAuth 2.0 for its authentication flows
(delegating login via authorization code, etc.)
and uses JWT as the format for ID Tokens (the bit that carries user identity).
In other words, OIDC is an identity layer over OAuth.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;UMA extends OAuth&lt;/strong&gt; – The User-Managed Access 2.0 specs build on OAuth 2.0 (and use bearer tokens)
to enable a resource owner consent workflow that goes beyond simple scopes,
allowing users to delegate fine-grained access to their data.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Newer extensions mix and match&lt;/strong&gt; – Recent drafts and extensions often combine pieces of the above.
For example, JWT access tokens (OAuth RFC 9068) marry OAuth with JWT/JOSE,
and the upcoming OAuth 2.1 consolidates core OAuth 2.0 with required security best practices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These relationships help illustrate why certain specs exist.
Some fill gaps in the core OAuth 2.0 spec
(like additional security, token management, or special flows for devices),
while others build higher-level functionality on top of OAuth
(like federated identity in OIDC or user-mediated sharing in UMA).&lt;/p&gt;

&lt;p&gt;With one glance at the graph, you can glean which specs are core vs. peripheral,
and how they logically group together.
This is super helpful when planning what to implement or what to study next.
Instead of treating the specs like an amorphous laundry list,
you start to see an architecture emerge: a few foundational layers, and many optional modules on top.&lt;/p&gt;

&lt;h2 id=&quot;beyond-tabs-gaining-insight-and-context&quot;&gt;Beyond Tabs: Gaining Insight and Context&lt;/h2&gt;

&lt;p&gt;Ultimately, this spec graph isn’t just about avoiding browser tab overload—it’s about
building a mental model of the OAuth/OIDC landscape.&lt;/p&gt;

&lt;p&gt;Instead of treating each RFC or spec as an isolated piece of the puzzle,
you start to see how the whole puzzle fits together.&lt;/p&gt;

&lt;p&gt;This has several benefits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Faster learning:&lt;/strong&gt; If you’re new to these standards,
a visual map helps you decide where to begin
(perhaps start with OIDC to see a full use-case,
then drill down into OAuth 2.0, then JWT… rather than reading specs in a random order).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Implementing with confidence:&lt;/strong&gt; When working on a feature (say, adding device login to your app),
you can consult the map to ensure you’ve pulled in all the relevant specs
(Device Authorization Grant, PKCE, maybe OAuth Security Best Practices) and understand their dependencies.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better communication:&lt;/strong&gt; As a developer or architect,
you can use this diagram to explain the ecosystem to colleagues.
It’s a lot easier to justify “we need to support spec X”
when you can show how X connects to the standards you already use.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Keeping up to date:&lt;/strong&gt; The OAuth/OIDC world is still evolving (OAuth 2.1, new best practices, etc.).
A living graph can be updated, so you can visually track new additions or changes in the framework over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I open-sourced the OIDC/OAuth Spec Graph with the hope that it will be a useful reference for others, not just myself.
If you think I missed an important spec or got a relationship wrong,
I’m all ears—feel free to open an issue or PR on the &lt;a href=&quot;https://github.com/ryanspletzer/oidc-oauth-spec-graph&quot;&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;What started as a personal exercise to manage my own spec overload has turned into a shareable artifact.&lt;/p&gt;

&lt;p&gt;I’ve found that understanding OAuth, OIDC, and their related specs
becomes much easier when you can see the forest rather than just the trees.&lt;/p&gt;

&lt;p&gt;After all, the goal is to make this stuff accessible—it shouldn’t require a PhD in browser tab management
to learn web authentication standards.&lt;/p&gt;

&lt;p&gt;Sometimes the best way to untangle complexity is to visualize it.&lt;/p&gt;
</content:encoded>
        <pubDate>Sun, 09 Nov 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/11/oidc-oauth-spec-graph/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/11/oidc-oauth-spec-graph/</guid>
        
        <category>openid-connect</category>
        
        <category>oauth</category>
        
        <category>jwt</category>
        
        
      </item>
    
      <item>
        <title>Pinocchio is Not a Real Boy</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>LLMs didn&apos;t make code literacy optional—they raised the bar for what you need to learn so that you can effectively steer these AI assisted tools.
</description>
        <content:encoded>&lt;p&gt;LLMs didn’t make code literacy optional—in fact, quite the opposite:
they raised the bar for what you need to learn
so that you can effectively steer these AI assisted tools.&lt;/p&gt;

&lt;p&gt;If anything, you’re going to have to study programming languages,
frameworks,
and systems design &lt;strong&gt;more&lt;/strong&gt; going forward,
not less.&lt;/p&gt;

&lt;p&gt;Because when a model spits out a slick-looking solution,
you still need to actually understand it.&lt;/p&gt;

&lt;p&gt;And if you’re going to ship that solution,
you need to be able to reason about it when something breaks at 2 A.M.&lt;/p&gt;

&lt;p&gt;Or worse, when it
&lt;a href=&quot;https://pivot-to-ai.com/2025/03/18/guys-im-under-attack-ai-vibe-coding-in-the-wild/&quot;&gt;gets hacked&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And when that happens,
you’ll have no one to point the finger at except yourself.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/pinocchio-is-not-a-real-boy.png&quot; alt=&quot;A wooden marionette with a long nose sits at a developer’s desk, strings tied to floating &amp;quot;PROMPT&amp;quot; and code icons, facing a monitor where a flow diagram ends at a glowing &amp;quot;DEPLOY&amp;quot; button; a loose cable and a shelf labeled &amp;quot;TOYS&amp;quot; underscore the fragile, toy-like nature of the build.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;this-shouldnt-be-possible-and-yet-it-is&quot;&gt;“This shouldn’t be possible”… and yet, &lt;em&gt;it is&lt;/em&gt;&lt;/h2&gt;

&lt;p&gt;I’m not a frontend specialist.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;I know HTML, CSS and enough raw JavaScript
with accompanying design patterns to be dangerous,
and I know enough about the fundamentals of the web
and about tracing down errors and bugs with browser web developer tools
to even identify and fix a problem;
but most of my web work is from 12+ years ago—back
when &lt;a href=&quot;https://en.wikipedia.org/wiki/Single-page_application&quot;&gt;SPA&lt;/a&gt; frameworks were
still figuring themselves out.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;And yet, despite my limited hands-on expertise in frontend development,
I recently used LLM-assisted development to contribute fixes
around unnecessary auth redirects to the identity provider from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;msal.js&lt;/code&gt;
in a team’s React/TypeScript frontend project,
and got them past the issue.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That ability to contribute to something
where I don’t even have strong enough expertise nor muscle memory
to do it by hand—with or without a lot of googling—is the magic of
AI-assisted code generation.&lt;/p&gt;

&lt;p&gt;And it is also the trap.&lt;/p&gt;

&lt;p&gt;This generated code came (largely) from an LLM-enabled software process,
that was carrying out my directives.&lt;/p&gt;

&lt;p&gt;It didn’t come from an intern or a junior developer or a non-technical person,
but it could have easily also come from anyone with any background,
provided they knew the right questions to ask…&lt;/p&gt;

&lt;p&gt;Despite knowing those right questions to ask,
and despite my very careful crafting of the prompt to describe precisely what I wanted,
and despite giving the tool a “short leash” by doing scoped edits in the relevant files,
I still found myself spending an undue amount of time reviewing the diff of the output
that came out of my prompt for this issue.&lt;/p&gt;

&lt;p&gt;I realized through this process that suddenly, &lt;em&gt;I&lt;/em&gt; was my own worst enemy.&lt;/p&gt;

&lt;p&gt;I had to study the output ~10x more carefully precisely because it felt “too good.”&lt;/p&gt;

&lt;p&gt;It turns out, the assistant made a couple of mistakes—of course it did—and
only by reading the code,
asking the right questions,
and generally just having a gut feeling that “something is not right here”
(“Why did it do &lt;em&gt;that&lt;/em&gt;?”),
did I catch the issues involved.&lt;/p&gt;

&lt;p&gt;The lesson here is:
AI coding assistance accelerates you very quickly into terrain you don’t fully know or understand.&lt;/p&gt;

&lt;p&gt;And because of that,
your only safety net is your willingness to continuously learn fast and verify everything.&lt;/p&gt;

&lt;h2 id=&quot;you-vibe-it-you-run-it&quot;&gt;You vibe it, you run it&lt;/h2&gt;

&lt;p&gt;There’s a line I like
from a &lt;a href=&quot;https://uptimelabs.io/you-vibe-it-you-run-it/&quot;&gt;recent blog post from Uptime Labs&lt;/a&gt;:
&lt;em&gt;“You vibe it, you run it.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I’m not here to out-write nor out-research that original essay;
I’m here to double down on the core points with lived experience.&lt;/p&gt;

&lt;p&gt;If you “vibe code” something you don’t understand and then hand it off,
&lt;strong&gt;no one&lt;/strong&gt; truly understands it.&lt;/p&gt;

&lt;p&gt;At best,
the next team inherits a black box with nice comments.&lt;/p&gt;

&lt;p&gt;At worst,
they inherit a fire.&lt;/p&gt;

&lt;p&gt;Yes,
you can ask an LLM to explain a codebase to you.&lt;/p&gt;

&lt;p&gt;You can be diligent with Cursor or GitHub Copilot rules,
ensuring that it always generates docstrings and Mermaid diagrams (token consumption galore).&lt;/p&gt;

&lt;p&gt;Some people will do that.&lt;/p&gt;

&lt;p&gt;Most inexperienced vibe coders won’t—at least, not yet.&lt;/p&gt;

&lt;p&gt;The folks who will do well and enjoy continued success in this era
aren’t the ones who ship the first demo
(which will become, on some level, commoditized to the point where anyone can do that);
they’re the ones with &lt;strong&gt;endless curiosity&lt;/strong&gt; who keep learning between prompts and after the demo.&lt;/p&gt;

&lt;p&gt;Because of this, I would really discourage using Max / YOLO Mode for everything
and not taking the time to understand what has been generated afterwards.&lt;/p&gt;

&lt;p&gt;I’ve dabbled in digital photography in the past,
and I’ll never forget what my friend’s dad (a professional photographer) said to me once:
“You know, the thing is, the more shots you take,
the more you have to go through and review later.”&lt;/p&gt;

&lt;p&gt;I learned that lesson early on when I was shooting photos with my DSLR,
and it’s even more of a prescient lesson now as I’m a few years older
and generally have less time to afford to give to endlessly reviewing and editing photos;
whether it’s a nice mirrorless camera or my phone’s camera,
I’m much more deliberate about what I snap a photo of now.&lt;/p&gt;

&lt;p&gt;In that vein, I recommend being much more deliberate about the “shots” you’re
taking with these coding tools.&lt;/p&gt;

&lt;p&gt;There are times when you want to generate something quickly from scratch as a starting point,
and letting the agent take over the whole project may be warranted in that initial prompt.&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Maybe the thing you’re generating is just for you and is simply a tool you’re going to run locally
and may even throw away later—fine.&lt;/p&gt;

&lt;p&gt;When things get “real” though, it’s a different story.&lt;/p&gt;

&lt;p&gt;You have to understand that the universe has enough entropy as it is,
and you’re not helping the situation by “pooping out” drivel and nonsense.&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;When you’re trying to bring something to life that may be used by others
and perhaps eventually put into a production setting,
I recommend orchestrating the LLM with a &lt;strong&gt;sliding scale of autonomy&lt;/strong&gt;:
ask relevant questions to the tool first to gain an understanding,
then based on that understanding try some approaches that tightly scope the autonomy of
the agent as possible for the problem at hand,
commit often so you have coherent states to roll back to
(and you can leverage the tools here by crafting more detailed commit messages, too),
take some suggestions from the tools;
but sometimes,
you need to let the tool sit there quietly while you read.&lt;/p&gt;

&lt;p&gt;In any case,
&lt;strong&gt;you&lt;/strong&gt; own the responsibility of comprehension afterwards.&lt;/p&gt;

&lt;h2 id=&quot;the-toy-that-went-to-production&quot;&gt;The toy that went to production&lt;/h2&gt;

&lt;p&gt;Every day I see vibe-coded projects deployed to the open internet with:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;no authentication&lt;/li&gt;
  &lt;li&gt;no authorization&lt;/li&gt;
  &lt;li&gt;unvalidated inputs&lt;/li&gt;
  &lt;li&gt;secrets in plain text&lt;/li&gt;
  &lt;li&gt;no TLS termination at the edge&lt;/li&gt;
  &lt;li&gt;no proper LB/WAF/DDOS protections&lt;/li&gt;
  &lt;li&gt;zero observability (no logs, no metrics, no traces)&lt;/li&gt;
  &lt;li&gt;no rollback plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In toy cases with silly frontends,
maybe this doesn’t matter.&lt;/p&gt;

&lt;p&gt;The problem is people are treating toys like production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pinocchio is not a real boy&lt;/strong&gt;,
and his nose gets longer every time he promises you
that his created puppet show is “good enough for prod.”&lt;/p&gt;

&lt;p&gt;Your creation will gather dust on a shelf—or worse,
you and Pinocchio will get swallowed by a whale,
in the form of your neighborhood friendly script kiddie
that’s taken over your website just for the lulz.&lt;/p&gt;

&lt;h2 id=&quot;study-more-not-less&quot;&gt;Study more, not less&lt;/h2&gt;

&lt;p&gt;I’m going to keep studying languages,
frameworks,
auth,
networking,
zero trust,
systems design patterns—because the work that actually matters isn’t a weekend toy.&lt;/p&gt;

&lt;p&gt;It’s lived in the messy world of legacy constraints,
enterprise identity,
real users,
and uptime.&lt;/p&gt;

&lt;p&gt;Knowing what you want counts for a lot.&lt;/p&gt;

&lt;p&gt;Knowing &lt;strong&gt;what it takes&lt;/strong&gt; to make it real counts for everything.&lt;/p&gt;

&lt;h2 id=&quot;quality-is-recognizableeven-to-your-contractor&quot;&gt;Quality is recognizable—even to your contractor&lt;/h2&gt;

&lt;p&gt;You’ve likely had a contractor open a wall at your home residence at some point and say,
“what the f— happened here?”&lt;/p&gt;

&lt;p&gt;That’s the sound of shortcuts and cut corners hitting their expiration date.&lt;/p&gt;

&lt;p&gt;You also know the feeling of opening a new iPhone and everything just… works.&lt;/p&gt;

&lt;p&gt;There’s a gap between “assembled parts” and “integrated product.”&lt;/p&gt;

&lt;p&gt;This is analogous to asking your buddy to build you a tower PC
when you have zero knowledge of how to build one yourself:
it sounds like a great idea and looks wonderful,
until a random part dies,
then suddenly you’re dependent on that one friend—or a repair shop—because
you don’t know how to replace the part yourself.&lt;/p&gt;

&lt;p&gt;A family member of mine is a commercial pilot with decades of experience,
and in recent years he has taken up the hobby of flying small drones,
and while he knows it is on some level a fun toy,
on another level he takes the FAA rules around drones very seriously.&lt;/p&gt;

&lt;p&gt;Further, he will likely never lose his drone in a lake
like you’ve seen in those many funny YouTube videos.&lt;/p&gt;

&lt;p&gt;He enjoys flying drones, but at the same time
he does &lt;strong&gt;not&lt;/strong&gt; trust them like a real aircraft.&lt;/p&gt;

&lt;p&gt;He understands capability versus consequence.&lt;/p&gt;

&lt;p&gt;We should, too.&lt;/p&gt;

&lt;h2 id=&quot;the-enterprise-intern-effect&quot;&gt;The enterprise “intern effect”&lt;/h2&gt;

&lt;p&gt;Even before the AI craze and hype hit,
at multiple companies,
I’ve long seen non-software engineering business teams hire a summer intern to build something the org won’t prioritize—and on the surface, this is often a perfectly reasonable move to explore.&lt;/p&gt;

&lt;p&gt;And the intern might do a great job for their level.&lt;/p&gt;

&lt;p&gt;But because the team around them has no software engineering experience,
they don’t understand the company’s ecosystem,
identity model,
deployment standards,
data policies, etc.&lt;/p&gt;

&lt;p&gt;When the internship ends,
so does ownership of the project.&lt;/p&gt;

&lt;p&gt;And the team is left holding the bag,
desperately asking other teams if they can take what the intern built
(which due to lack of guidance is &lt;em&gt;nowhere&lt;/em&gt; near being production-ready)
and “host” it somewhere besides the local laptop it was built on…&lt;/p&gt;

&lt;p&gt;That pattern is now widespread—a director of marketing with no programming experience
can now vibe code a solution into reality
(and even commit it to git—I’ve seen it happen!)
and the “handoff” from the coding tool to person with the idea is immediate.&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;It’s like everyone now has their own personal intern,
who can do an entire intern project in a day.&lt;/p&gt;

&lt;p&gt;But it turns out,
the intern is a model,
and much like a real-life software engineering intern
embedded in a team of non-software engineers,
there probably wasn’t the right guidance available
during those vibe coding sessions…&lt;/p&gt;

&lt;p&gt;If you don’t build the &lt;strong&gt;muscle&lt;/strong&gt; of understanding,
you get all of the demo and none of the durability.&lt;/p&gt;

&lt;h2 id=&quot;yes-you-still-have-to-study&quot;&gt;Yes, you still have to study&lt;/h2&gt;

&lt;p&gt;Notice the wave of new O’Reilly-style books and courses popping up:
“Build X with Y,”
“Web Apps with Bolt,”
“Secure LLM Apps on Z.”&lt;/p&gt;

&lt;p&gt;If you thought “AI means I don’t have to study,”
think again.&lt;/p&gt;

&lt;p&gt;The material is evolving because the &lt;strong&gt;work&lt;/strong&gt; is evolving,
and the work still rewards people who can read code,
reason about systems,
and make good trade-offs.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;We can all experiment.&lt;/p&gt;

&lt;p&gt;And we should.&lt;/p&gt;

&lt;p&gt;But experiments don’t relieve us from the burden of understanding.&lt;sup id=&quot;fnref:7&quot;&gt;&lt;a href=&quot;#fn:7&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Pinocchio can sing,
dance,
and put on a great demo.&lt;/p&gt;

&lt;p&gt;Just remember:
he’s not a real boy.&lt;/p&gt;

&lt;p&gt;He’s not even a puppet with sentience—just
a wooden marionette where you are pulling the strings
and telling him the songs to perform.&lt;/p&gt;

&lt;p&gt;And his nose grows longer with every lie he tells you
about the production-worthiness of the things you are vibing into existence.&lt;/p&gt;

&lt;p&gt;If the songs he sings are poorly written and out of tune,
no one will be willing to pay for the puppet show
(or give you the time of day to put the show on in the first place),
and you will have no one to blame but yourself.&lt;/p&gt;

&lt;p&gt;In this way, vibe coding (and to some degree, using AI in general)
is very much like holding a mirror up to yourself.&lt;/p&gt;

&lt;p&gt;Yes, it will allow you to do things you never thought possible.&lt;/p&gt;

&lt;p&gt;But it will also reflect back the gaps in your own understanding,
especially if you lack the self-awareness to recognize your own weaknesses
that prevent you from fostering curiosity and asking the right questions.&lt;/p&gt;

&lt;p&gt;Just because you can will something into existence,
does not mean that you are absolved of personal responsibility over what you created.&lt;/p&gt;

&lt;p&gt;If you want to build something you can trust,
you still have to do the work—study harder,
continuously learn,
stay curious,
give the AI a shorter leash (when possible),
and &lt;strong&gt;own what you vibe&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;footnotes&quot;&gt;Footnotes&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;But I’m working on it. &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;Sidebar, for the continous learners out there, &lt;a href=&quot;https://smacss.com/&quot;&gt;Scalable and Modular Architecture for CSS&lt;/a&gt; will change your life. It certainly might not be the most modern tome on CSS best practices, but it certainly changed my entire perspective on what maintainable CSS could look like when I read it a number of years ago. &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;Really, a large part of contribution to that frontend fix was in large part my deep, years-long expertise in OpenID Connect/OAuth 2.0/JWT tokens, etc., but I digress. &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
      &lt;p&gt;I find spec-driven development fascinating in this regard, because the act of forcing yourself to write out a spec goes a long way towards helping to craft your understanding of the problem, and the code that follows—but this still doesn’t absolve you of understanding the generated output. &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
      &lt;p&gt;I didn’t invent the “pooping out code” phrase—many years ago, far before the dawn of AI coding assistance, I had a colleague who enjoyed scaffolding tools a &lt;em&gt;lot&lt;/em&gt; and wanted the tools to just “poop out the code for me.” &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
      &lt;p&gt;I’ve seen these patterns play out before, too, with low-code/no-code tools and the notion of “citizen developers”—even if something is created that is well-done, at a certain point I have often observed that the citizen developer doesn’t want to maintain the thing they have built indefinitely, and really wants to hand it off to some other group to run and maintain. &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:7&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=Ssd3U_zicAI&quot;&gt;What’s so funny ‘bout peace, love, and understanding?&lt;/a&gt; &lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sat, 06 Sep 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/09/pinocchio-is-not-a-real-boy/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/09/pinocchio-is-not-a-real-boy/</guid>
        
        <category>vibe-coding</category>
        
        <category>ai</category>
        
        
      </item>
    
      <item>
        <title>Ask vs Act: Applying CQRS Principles to AI Agents</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Asking an AI agent a question is a whole different ballgame from letting it take action.
</description>
        <content:encoded>&lt;p&gt;Asking an AI agent a question is a whole different ballgame from letting it take action.&lt;/p&gt;

&lt;p&gt;Imagine chatting with an AI assistant that not only answers your questions
but can also perform tasks on your behalf.
For example, you ask, “What’s our PTO policy?” and get a helpful answer—and then you say,
“Please submit my PTO request for next week,” and the agent actually goes off to do it.
These two requests might seem similar (both are things you ask the AI),
but under the hood they are very different operations.
In software architecture, we have a name for recognizing and handling this difference:
&lt;strong&gt;CQRS&lt;/strong&gt;, which stands for &lt;em&gt;Command Query Responsibility Segregation&lt;/em&gt;.
CQRS essentially says we should split the world into &lt;strong&gt;queries&lt;/strong&gt; (reads) and &lt;strong&gt;commands&lt;/strong&gt; (writes)
and handle each with its own pathway.&lt;sup id=&quot;fnref:1&quot;&gt;&lt;a href=&quot;#fn:1&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;
In this post, we’ll explore how CQRS concepts apply to building AI agents—and
why &lt;strong&gt;retrieving information&lt;/strong&gt; (think of Retrieval-Augmented Generation, or RAG)
versus taking &lt;strong&gt;actions&lt;/strong&gt; (executing some operation)
need to be treated differently at every step.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/ask-vs-act-applying-cqrs-principles-to-ai-agents.png&quot; alt=&quot;A conceptual diagram titled &amp;quot;Ask vs Act: Applying CQRS Principles to AI Agents.&amp;quot; The diagram has two halves. On the left, a blue bubble labeled &amp;quot;Reading&amp;quot; connects downward to a smaller bubble labeled &amp;quot;RAG,&amp;quot; which points to an icon representing documents and a database. On the right, an orange bubble labeled &amp;quot;Writing&amp;quot; connects downward to a smaller bubble labeled &amp;quot;Actions,&amp;quot; which points to icons of a checkmark in a speech bubble and a gear. The two halves are connected side by side, visually contrasting how AI agents handle reading (retrieval) versus writing (actions).&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-is-cqrs&quot;&gt;What is CQRS?&lt;/h2&gt;

&lt;p&gt;CQRS is an architectural pattern that &lt;strong&gt;separates the responsibility
of reading data from writing data&lt;/strong&gt;.
In traditional systems,
you often have one architectural stack or service handling both reads and writes,
which can lead to all sorts of issues—performance bottlenecks, scaling difficulties,
and conflicts between read and write workloads.
CQRS addresses this by using &lt;strong&gt;different architectural models
(or even different services and data stores)
for writes versus reads&lt;/strong&gt;.
In other words, the part of your system that processes &lt;strong&gt;commands&lt;/strong&gt;
(things that change state)
is kept separate from the part that handles &lt;strong&gt;queries&lt;/strong&gt;
(things that only retrieve data).
This allows each side to be optimized and scaled independently.
For example, the write side (commands) can focus on &lt;strong&gt;transactional integrity
and business logic&lt;/strong&gt;,
ensuring every change follows the rules and is consistent,
while the read side (queries) can be optimized for &lt;strong&gt;fast lookups
and flexible querying&lt;/strong&gt;,
often using techniques like caching or denormalized view models.
The result is a more robust system:
each half does one job and does it well,
without stepping on the other’s toes.&lt;/p&gt;

&lt;p&gt;Why does this matter for AI?
Modern AI agents are, at their core,
software systems that &lt;strong&gt;both&lt;/strong&gt; answer questions &lt;strong&gt;and&lt;/strong&gt; perform actions.
If we draw a parallel,
an AI agent answering your question (“What’s the PTO policy?”)
is akin to a &lt;strong&gt;query&lt;/strong&gt;—it’s reading from some knowledge source
and returning information.
But when that agent performs an action (“Submit PTO request”),
it’s like a &lt;strong&gt;command&lt;/strong&gt;—it’s trying to change the state of some system
(your HR system in this case).
The CQRS principle tells us it’s wise to handle those two types of operations separately.
Just as in a well-architected app we wouldn’t use the exact same process
to both fetch data and update a database,
in an AI agent we shouldn’t treat information retrieval
the same as we treat action execution.
We’ll delve into exactly how to separate these in an AI context,
but first let’s break down the two sides:
the &lt;strong&gt;read path&lt;/strong&gt; (our AI agent’s knowledge retrieval, often using RAG)
and the &lt;strong&gt;write path&lt;/strong&gt; (the agent’s action-taking via tools or APIs).&lt;/p&gt;

&lt;h2 id=&quot;the-read-path-retrieval-augmented-generation-rag&quot;&gt;The Read Path: Retrieval-Augmented Generation (RAG)&lt;/h2&gt;

&lt;p&gt;When an AI agent answers questions or gathers context,
it’s operating on the &lt;strong&gt;read side&lt;/strong&gt; of the world—no different from
a user running a search or query.
A popular approach for AI agents to answer user questions
is &lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt;.
RAG is essentially a two-step process:
first retrieve &lt;em&gt;relevant&lt;/em&gt; content from your knowledge sources,
then have the AI model &lt;em&gt;generate&lt;/em&gt; an answer using that content.
In practice, this means the agent might take your question,
use it to look up documents or data
(for example, searching a vector database of internal docs,
or hitting a knowledge base API,
perhaps via an MCP proxy endpoint),
and then compose a response based on what it found.
Crucially, this read process &lt;strong&gt;does not alter any system state&lt;/strong&gt;—it’s
pulling information, not changing information.&lt;/p&gt;

&lt;p&gt;Because reads are read-only,
we have a lot of flexibility in where the data comes from.
An AI agent’s retrieval step can tap into all sorts of sources:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Operational data sources:&lt;/strong&gt;
Sometimes the freshest or most authoritative info lives in operational systems
(e.g. an airline reservation system for up-to-the-minute flight status).
In fact, many real-world RAG setups use a hybrid approach
that draws on structured operational data
(like databases, CRMs, or transactional systems)
to fetch precise facts (customer records, inventory levels, latest transactions)
in addition to unstructured docs.&lt;sup id=&quot;fnref:2&quot;&gt;&lt;a href=&quot;#fn:2&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Analytical or external knowledge bases:&lt;/strong&gt;
Other times the agent might query analytical data products or data warehouses—for
example, to get aggregated metrics or historical trends—or
use a vector search index populated with company documents for semantic search.
The key is that for reads,
&lt;em&gt;it’s fair game to pull from either operational or analytical stores&lt;/em&gt;.
(But there are considerations as to when to go to one versus the other.)
The agent could query a Snowflake dataset for last quarter’s sales number
and also query a vector DB for the latest policy document text.
As long as the data is accessible (and not too stale to be irrelevant),
the agent can use it.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing to keep in mind is that the quality of these read-side sources
directly affects the agent’s output.
If the underlying knowledge is outdated or disorganized,
the AI’s answer will suffer—garbage in, garbage out.
AI will confidently return an outdated policy
as an answer because an old document hadn’t been marked obsolete
in the knowledge index.
So, even though reads don’t modify anything,
they do require us to think about data curation and freshness
(much like any search system).
But from a &lt;em&gt;system design&lt;/em&gt; perspective,
reads are the “easy” part:
they’re &lt;em&gt;fast, parallelizable, and forgiving&lt;/em&gt;.
You can cache results,
pre-compute indexes (which in a way is analogous to embeddings for vector search),
and generally throw engineering tricks at making reads as efficient as possible.
If a read query hits a slightly stale replica,
usually it’s not the end of the world (but this depends on the scenario)—you
might just get a slightly outdated answer,
which can be corrected next time or with a refresh.
This tolerance is one reason CQRS often allows the read model
to be eventually consistent with the write model.
The takeaway is that an AI agent’s RAG-based retrieval is its &lt;strong&gt;query model&lt;/strong&gt;—and
we can scale and optimize that independently from any action-taking logic.&lt;/p&gt;

&lt;h2 id=&quot;the-write-path-taking-actions-commands-with-consequences&quot;&gt;The Write Path: Taking Actions (Commands with Consequences)&lt;/h2&gt;

&lt;p&gt;Now let’s talk about the more &lt;strong&gt;consequential side&lt;/strong&gt; of the house:
when an AI agent takes an action that changes something in the world.
This is the &lt;strong&gt;write path&lt;/strong&gt;,
analogous to the command side in CQRS.
In an AI agent context,
a “write” usually means invoking some tool or API call
that performs an operation—booking a meeting,
creating a support ticket, updating a database record,
sending an email, you name it.
Unlike a read, which might draw on multiple sources,
a write typically must go to a specific operational system—the
system of record that actually accepts the change.
If our agent is to submit a PTO request,
that needs to happen in the HR system
(the authoritative operational source for PTO data),
not in, say, a reporting data warehouse.
Writes have to hit the &lt;strong&gt;source of truth&lt;/strong&gt;
so that the action is real and persistent.&lt;/p&gt;

&lt;p&gt;(As an aside:
lately I’ve also been referring to the source of truth
using an alternative term that may be familiar to those who know DNS:
the &lt;strong&gt;source of authority&lt;/strong&gt;.
DNS records have a source of authority on the internet—often
at a domain registrar—and
DNS records are propagated out to other DNS servers on the internet.
All the requests for DNS queries to any DNS server
return records that are “true,”
but only the origin DNS servers are the “source of authority,”
represented by SOA records in the DNS system.
When a change is made in a DNS record at the source of authority,
there is a brief period of time
where that change needs to propagate out to other servers,
and &lt;em&gt;eventually&lt;/em&gt; it becomes consistent throughout the internet.
Relating this back to data:
I find there is often confusion around what data is “true.”
Data may very well be replicated to multiple places in a given ecosystem,
very typically to analytical data platforms—and
the data at all those locations all is typically “true”!—however,
there’s only &lt;em&gt;one&lt;/em&gt; place where the data is “authoritative.”
Inside the words “authoritative” and “authority” is the word “author,”
so another way to think of this is:
the source of authority for data is typically
where the data is &lt;em&gt;authored&lt;/em&gt;,
and that typically makes it the “authoritative” source of the data.
Following this, much like DNS records,
when an authoring change is made to data,
it takes a little bit of time for it to eventually replicate
and become consistent in a given ecosystem,
typically through combinations of periodic ELT jobs
or event-driven syncs.)&lt;/p&gt;

&lt;p&gt;Because actions carried by agents actually change the state of our systems,
they come with a lot more responsibility and required care:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Business Logic and Validation:&lt;/strong&gt;
Operational systems don’t (and shouldn’t) allow just any change willy-nilly.
There are business rules and validations in place.
For example, submitting a PTO request might require
that the dates don’t clash with a holiday,
or that the user has enough PTO balance, etc.
An AI agent needs to respect those rules.
If you simply feed the LLM a text instruction
“ensure the user has enough PTO days”
and hope it does the right thing,
you’re taking a big risk.
As industry experts have noted,
relying on an LLM alone to enforce complex business policies is risky—the
model might misunderstand the rule or be manipulated by a crafty prompt.
Therefore, the write path often involves explicit checks
or calls to validation services.
In a sense, the &lt;strong&gt;write model&lt;/strong&gt; of our AI agent includes
this &lt;em&gt;deterministic guardrail logic&lt;/em&gt; to make sure the action is valid
(just as a traditional write model in CQRS would encapsulate
all the business rules for a transaction).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Confirmation and Safety:&lt;/strong&gt;
Another key difference with writes is the potential impact of getting it wrong.
A bad read (e.g., mis-answering a question) might confuse or annoy a user,
but a bad write could create real damage—think
of an agent accidentally ordering 1000 units of something
or deleting the wrong user account. 😬
For this reason, it’s wise to &lt;strong&gt;confirm actions with a human&lt;/strong&gt; before executing them,
especially in a chat or interactive setting.
Many agent frameworks build this in.
For instance, Amazon’s agent toolkit suggests requiring the user’s explicit confirmation
before an agent executes certain functions,
precisely to safeguard against malicious or unintended commands.&lt;sup id=&quot;fnref:3&quot;&gt;&lt;a href=&quot;#fn:3&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;
In our PTO example, the agent should probably respond with
“I can submit a PTO request for you from Sept 25 to Sept 29, 2025.
Shall I go ahead and do that?”—only
on a “Yes” or a confirmation through a button click
should it actually call the PTO API.
This aligns with a broader principle:
&lt;strong&gt;no autonomous writes without user oversight&lt;/strong&gt;
(at least not until we’re very confident in the agent’s judgment).
In non-chat scenarios, this might be implemented via a human-in-the-loop queue
or approval workflow for agent-initiated actions.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Transactional Integrity:&lt;/strong&gt;
When the agent does perform the action,
it needs to handle success or failure robustly.
The operational system might reject the request
(e.g., if the user didn’t have enough PTO days,
the HR API will return an error).
The agent’s write path has to be ready to catch errors,
possibly roll back or try a different approach,
and inform the user gracefully.
This is analogous to how a well-designed command handler in a system
ensures that either the transaction fully succeeds
or it cleanly fails and reports back.
In an agent, you don’t want it to just silently fail
or, worse, assume success when the operation actually didn’t go through.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these factors mean the write path is usually more complex
and needs tighter control than the read path.
In CQRS terms, the &lt;strong&gt;command side is optimized for correctness,
consistency, and safeguards&lt;/strong&gt;, not raw speed.
We often accept a bit more latency or friction for writes
(like an extra confirmation step or a validation call),
because the priority is &lt;strong&gt;doing the right thing&lt;/strong&gt; and maintaining system integrity.
Indeed, one of the benefits of classic CQRS is that your write model
can be built with all the heavy checks and balances needed,
while your read model remains snappy for queries.
We see the same in AI agents:
the retrieval (reading) can be free-flowing and creative,
but when it’s time to execute an action,
the agent should switch into a more &lt;strong&gt;controlled, rule-abiding mode&lt;/strong&gt;.&lt;/p&gt;

&lt;h2 id=&quot;designing-ai-agent-architecture-with-separate-readwrite-pipelines&quot;&gt;Designing AI Agent Architecture with Separate Read/Write Pipelines&lt;/h2&gt;

&lt;p&gt;Embracing CQRS in an AI agent doesn’t necessarily mean
you literally have two completely separate codebases,
but it does mean &lt;strong&gt;architecting the agent’s “brain” to
have a clear split between reading from knowledge versus acting on the world&lt;/strong&gt;.
Practically, this often translates to using different modules or components
in your agent system.
For example:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;You might have a &lt;strong&gt;retriever module&lt;/strong&gt; (or chain)
that handles all RAG-related tasks:
embedding the query, searching the vector database or other data sources,
and returning supporting facts.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Then a separate &lt;strong&gt;action module&lt;/strong&gt; (or planner/executor)
that decides when and how to invoke tools or APIs to make changes,
and handles the responses from those actions.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some emerging frameworks make it easier to build this kind of separation.
One example is &lt;strong&gt;LangGraph&lt;/strong&gt;&lt;sup id=&quot;fnref:4&quot;&gt;&lt;a href=&quot;#fn:4&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;4&lt;/a&gt;&lt;/sup&gt;,
an open-source library built on the core of LangChain&lt;sup id=&quot;fnref:5&quot;&gt;&lt;a href=&quot;#fn:5&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;
that lets you define &lt;strong&gt;agent workflows as graphs&lt;/strong&gt;.
With LangGraph, you can explicitly model the agent’s decision process
as nodes and edges—for instance,
one branch of the graph could be a “Retrieve knowledge” step
and another branch a “Perform action” step,
each with its own logic.
Crucially, LangGraph was designed with &lt;strong&gt;human-agent collaboration&lt;/strong&gt; in mind:
it enables agents to &lt;strong&gt;write drafts for review and await approval&lt;/strong&gt; before acting,
and makes it easy to insert human-in-the-loop checks for any action.
In other words, it provides a structured way to say
“this part of the agent’s workflow is a &lt;em&gt;query&lt;/em&gt; (no approval needed),
and this part is a &lt;em&gt;command&lt;/em&gt; (pause and get approval)”.
Even if you don’t use such a framework,
the concept is clear—treat
the action-taking part as a first-class concern,
with its own state and control flow,
separate from the Q&amp;amp;A part.&lt;/p&gt;

&lt;p&gt;Another consideration is data flow between these components.
Often the result of the read path will inform the write path.
For example, if the user asks, “Order more laptops for the team,”
the agent might first do a &lt;em&gt;read&lt;/em&gt; step:
query an inventory or procurement knowledge base
to see what the current stock and policy is.
Based on that, it forms a plan and then triggers the &lt;em&gt;write&lt;/em&gt; step:
calling the procurement system’s API to place the order.
By separating these concerns,
you can even choose to run them on different infrastructure—the
retrieval might run against a vector search service or analytical database,
whereas the action might run via a secure connector to the operational system.
(It should go without saying that reading from an out-of-date analytical database
&lt;em&gt;may&lt;/em&gt; be a bad idea if you need that data to be accurate up-to-the-minute
for the subsequent write operation;
therefore in agents with action capabilities,
it may be better to read from the operational system first
before performing the action,
but other standalone read prompts may be just fine reading from an analytic system.
Use your best judgment here,
and always do a “sanity check” read of the operational data store
before doing a write!)
In a way, this mirrors the idea of having separate read and write databases
in advanced CQRS implementations&lt;sup id=&quot;fnref:6&quot;&gt;&lt;a href=&quot;#fn:6&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;6&lt;/a&gt;&lt;/sup&gt;
(for instance, a denormalized read store vs. a normalized transactional write store).
In AI agents, you might have a &lt;strong&gt;vector DB + cache&lt;/strong&gt; for reads
and a set of &lt;strong&gt;authenticated API clients&lt;/strong&gt; for writes.&lt;/p&gt;

&lt;h2 id=&quot;key-differences-between-reading-and-writing-rag-vs-actions-in-agents&quot;&gt;Key Differences Between Reading and Writing (RAG vs Actions) in Agents&lt;/h2&gt;

&lt;p&gt;To summarize the lessons of applying CQRS to AI agents,
let’s highlight the key differences and best practices
when handling “reads” versus “writes” in an agent:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Data Sources and Freshness:&lt;/strong&gt;
&lt;em&gt;Reads&lt;/em&gt; (RAG) can draw from a wide array of sources—from
real-time operational databases to analytical warehouses
or indexed documents—to gather context.
&lt;em&gt;Writes&lt;/em&gt; (actions) must target the &lt;strong&gt;authoritative operational system&lt;/strong&gt;
for the task at hand.
If an agent needs to record or change data,
it has to go through the system-of-record
(be it an HR system, CRM, etc.),
not a read-only cache or replica (obviously).
This ensures the action is reflected in the source of truth.
(In short: read from anywhere that makes sense,
but write only to where it truly counts.)&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Performance vs. Integrity:&lt;/strong&gt;
&lt;em&gt;Reads&lt;/em&gt; are optimized for performance—low
latency, high throughput,
and often eventual consistency is acceptable.
You might use denormalized data or briefly stale indices to answer quickly.
&lt;em&gt;Writes&lt;/em&gt; prioritize &lt;strong&gt;integrity and correctness&lt;/strong&gt; over raw speed.
It’s better for a write action to be a bit slower or go through checks
than to be fast and wrong.
The write path should include all necessary validation
and follow the business process exactly
(e.g., a “Book hotel room” command goes through all the steps
a proper booking requires).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Business Logic Enforcement:&lt;/strong&gt;
&lt;em&gt;Reads&lt;/em&gt; typically involve minimal business logic—mostly
filtering or formatting data—since
they don’t change anything.
&lt;em&gt;Writes&lt;/em&gt; carry the full weight of business rules.
The agent’s action module needs to &lt;strong&gt;enforce policies and rules&lt;/strong&gt;
either by invoking back-end validations or through explicit logic.
As noted, don’t trust the LLM to enforce all rules implicitly.
Instead, encode critical rules in code or use external validators
to ensure compliance
(for example, require certain fields or approvals for specific actions).
Some companies are even developing “policy engines” for AI agents
to reliably translate high-level rules
(“User must have VP approval to spend over $10k”)
into checks before an action executes.&lt;sup id=&quot;fnref:7&quot;&gt;&lt;a href=&quot;#fn:7&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Error Handling and Feedback:&lt;/strong&gt;
On the &lt;em&gt;read&lt;/em&gt; side, if something goes wrong
(say a data source is unreachable),
the agent can often fail gracefully—maybe
answer “I don’t have that info right now,”
or just produce an answer with what it has.
On the &lt;em&gt;write&lt;/em&gt; side, failures need careful handling.
The agent should catch errors from the tool/API
(e.g., insufficient permissions, validation failed, network timeout)
and decide on next steps:
maybe ask the user for a different input,
try an alternate method,
or at least report the failure.
In essence, write operations in an agent should be treated like transactions—with
commit/rollback semantics or compensating actions if possible
(this is analogous to how a robust CQRS command side
might use techniques like sagas&lt;sup id=&quot;fnref:8&quot;&gt;&lt;a href=&quot;#fn:8&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;8&lt;/a&gt;&lt;/sup&gt; to handle failures,
especially for very complex actions that could span multiple systems).
One of your agent’s actions &lt;em&gt;will&lt;/em&gt; fail at some point,
whether it’s due to an upstream outage or a transient failure,
and you want to ensure you leave your systems in a consistent state,
and also be able to show the best possible error message you can to the user,
and further see if there is a graceful way to provide them an alternative method
for carrying out their task.
For example: if the agentic action fails,
maybe you can provide the means to retry if it was a transient error,
or maybe you can redirect the user to a viable non-agentic alternative,
or send them to a relevant support channel,
connect them with a human,
or many other potential alternatives.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;User Confirmation and Trust:&lt;/strong&gt;
&lt;em&gt;Reads&lt;/em&gt; can usually be executed automatically without user intervention—you
don’t need to ask permission to look something up
(aside from access control concerns).
&lt;em&gt;Writes&lt;/em&gt; should &lt;strong&gt;earn trust&lt;/strong&gt;.
It’s a good practice to involve the user
when an agent is about to do something non-trivial or irreversible.
As we discussed, a simple confirmation step (“Are you sure?”) goes a long way.
In more complex workflows,
a human reviewer might need to approve a batch of actions
the agent wants to perform.
The goal is to keep a &lt;strong&gt;human in the loop&lt;/strong&gt; for oversight,
until we’re as confident in the AI as we are in autopilot flying a plane. 😉
Even advanced frameworks encourage this—for example,
LangGraph allows inserting moderation or approval nodes
so that certain agent actions pause and wait for a human to OK them.
This not only prevents mistakes or mischief
but also improves user confidence that the AI isn’t running wild.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By acknowledging these differences,
we essentially set up our AI agent with two mental models:
one for &lt;em&gt;retrieving knowledge&lt;/em&gt; and one for &lt;em&gt;executing decisions&lt;/em&gt;.
This duality is exactly what CQRS preaches,
applied to the realm of AI-driven systems.&lt;/p&gt;

&lt;h2 id=&quot;conclusion-embracing-the-separation-of-reads-and-writes-in-ai-agents&quot;&gt;Conclusion: Embracing the Separation of Reads and Writes in AI Agents&lt;/h2&gt;

&lt;p&gt;The main takeaway here is simple but profound:
&lt;strong&gt;asking&lt;/strong&gt; and &lt;strong&gt;acting&lt;/strong&gt; are fundamentally distinct operations,
and we should design AI agents with that distinction front and center.
Borrowing the CQRS mindset helps because it reminds us
to use the right tools and safeguards for each job.
An AI agent leveraging RAG is fantastic at pulling in information
(sometimes from all corners of your enterprise data),
but that same agent needs a very different approach
when it comes time to &lt;strong&gt;do something&lt;/strong&gt; with that information.
In practice, this means building agents that have clear “query modes”
and “command modes,” with appropriate pipelines for each.
Reads can be liberal—grab
whatever data might help, combine it, summarize it—while
writes must be conservative—follow
the rules, double-check, get approval, log everything.&lt;/p&gt;

&lt;p&gt;As AI agents become more powerful and autonomous,
this separation will only grow in importance.
We’ve all seen the hype around agents that can plan and execute tasks;
it’s exciting, but without a grounded architecture
it can also be a recipe for chaos.
By treating reads and writes differently
(like different species in the AI ecosystem),
we ensure that our agents can be both &lt;strong&gt;useful and trustworthy&lt;/strong&gt;.
They’ll give great answers and perform reliable actions,
without confusing the two.
In essence, we’re teaching our AI agents a lesson
that seasoned software architects have known for years:
&lt;strong&gt;there’s a time to observe, and a time to act—and
handling each properly makes all the difference&lt;/strong&gt;.
Adopting a CQRS-like approach for AI won’t stifle their abilities;
on the contrary, it will let us scale up their knowledge powers
and automation powers side by side, safely and effectively.&lt;sup id=&quot;fnref:9&quot;&gt;&lt;a href=&quot;#fn:9&quot; class=&quot;footnote&quot; rel=&quot;footnote&quot; role=&quot;doc-noteref&quot;&gt;9&lt;/a&gt;&lt;/sup&gt;
So the next time you design an AI agent, remember to ask yourself:
&lt;em&gt;Which parts of this are queries, and which are commands?&lt;/em&gt;
By designing with that question answered,
you’ll be well on your way to an agent that can both speak intelligently
and act responsibly.&lt;/p&gt;

&lt;p&gt;(For more information on the patterns in this space,
I encourage the reader to dig into the following software engineering principles, in this order:
SOLID, Domain Driven Design, CQRS and Event Sourcing.
These principles build on each other and
will help you to find your way through building these complex agentic systems.
Some of the best books on this subject that bring this all together are oldies but goodies,
and I have them listed on my &lt;a href=&quot;/linkfarm&quot;&gt;linkfarm&lt;/a&gt; page:
&lt;a href=&quot;https://www.amazon.com/Adaptive-Code-principles-Developer-Practices/dp/1509302581/&quot;&gt;Adaptive Code: Agile coding with design patterns and SOLID principles (2nd Edition) (Developer Best Practices)&lt;/a&gt; and
&lt;a href=&quot;https://www.amazon.com/Microsoft-NET-Architecting-Applications-Enterprise/dp/0735685355&quot;&gt;Microsoft .NET - Architecting Applications for the Enterprise (2nd Edition) (Developer Reference)&lt;/a&gt;.
Whether or not you’ve worked in the Microsoft ecosystem and with the languages described within those texts,
the patterns are relevant to many languages and frameworks and software stacks.)&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;div class=&quot;footnotes&quot; role=&quot;doc-endnotes&quot;&gt;
  &lt;ol&gt;
    &lt;li id=&quot;fn:1&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://dev.to/cadienvan/cqrs-separating-the-powers-of-read-and-write-operations-in-event-driven-systems-47eo#:~:text=The%20CQRS%20,CQRS%20pattern%20in%20more%20detail&quot;&gt;CQRS: Separating the Powers of Read and Write Operations in Event-Driven Systems&lt;/a&gt; &lt;a href=&quot;#fnref:1&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:2&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://seanfalconer.medium.com/ai-wont-save-you-from-your-data-modeling-problems-cb1280cc6a37&quot;&gt;AI Won’t Save You From Your Data Modeling Problems&lt;/a&gt; &lt;a href=&quot;#fnref:2&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:3&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://docs.aws.amazon.com/bedrock/latest/userguide/agents-userconfirmation.html#:~:text=You%20can%20safeguard%20your%20application,actions%20are%20implemented%20only%20after&quot;&gt;Get user confirmation before invoking action group function&lt;/a&gt; &lt;a href=&quot;#fnref:3&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:4&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.langchain.com/langgraph&quot;&gt;LangGraph&lt;/a&gt; &lt;a href=&quot;#fnref:4&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:5&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.langchain.com/&quot;&gt;LangChain&lt;/a&gt; &lt;a href=&quot;#fnref:5&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:6&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs&quot;&gt;CQRS Pattern - Azure Architecture Center on Microsoft Learn&lt;/a&gt; &lt;a href=&quot;#fnref:6&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:7&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.moveworks.com/us/en/resources/blog/how-policy-validators-help-ai-compliance#:~:text=Read%20whitepaper&quot;&gt;Moveworks Policy Validators: Agentic AI, Built for Compliance&lt;/a&gt; &lt;a href=&quot;#fnref:7&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:8&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/azure/architecture/patterns/saga&quot;&gt;Saga Design Pattern - Azure Architecture Center on Microsoft Learn&lt;/a&gt; &lt;a href=&quot;#fnref:8&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
    &lt;li id=&quot;fn:9&quot;&gt;
      &lt;p&gt;&lt;a href=&quot;https://www.merge.dev/blog/rag-vs-ai-agent#:~:text=RAG%20allows%20users%20to%20ask,doesn%E2%80%99t%20use%20an%20AI%20agent&quot;&gt;AI agent vs RAG: how the two differ and where they overlap&lt;/a&gt; &lt;a href=&quot;#fnref:9&quot; class=&quot;reversefootnote&quot; role=&quot;doc-backlink&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
    &lt;/li&gt;
  &lt;/ol&gt;
&lt;/div&gt;
</content:encoded>
        <pubDate>Sun, 17 Aug 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/08/ask-vs-act-applying-cqrs-principles-to-ai-agents/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/08/ask-vs-act-applying-cqrs-principles-to-ai-agents/</guid>
        
        <category>agents</category>
        
        <category>ai</category>
        
        <category>cqrs</category>
        
        
      </item>
    
      <item>
        <title>MCP is a USB Port, Not a Hard Drive</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Or: why your &quot;just grab every Slack link since 2017&quot; request is going to hurt.
</description>
        <content:encoded>&lt;p&gt;&lt;em&gt;Or:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Why your “just grab every message in this Slack channel since 2017” request is going to hurt.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;A funny thing happens any time a new standard or protocol lands in the AI world:
people assume it magically solves everything upstream and downstream of it.
Anthropic’s Model Context Protocol (MCP) is the latest occurrence of this.
I keep hearing versions of:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Sweet, so with Slack MCP I can just ask Claude to give me all links anyone ever posted in the #eng channel, right?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sure. This is like saying you “just” drink from a firehose with a coffee straw.&lt;/p&gt;

&lt;p&gt;This post is my attempt to de-hype MCP—not because it’s bad (it’s actually great!),
but because it’s being asked to do jobs it was never designed for,
and we are collectively “hallucinating” that it can give us lots of things “for free”
without a lot of elbow grease behind-the-scenes.
Think of this as a sanity guide for folks building AI integrations,
and a reminder that protocols don’t magically turn rate‑limited APIs into data lakes.
It’s all about the difference between operational vs. analytical use cases…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/mcp-is-a-usb-port-not-a-hard-drive.png&quot; alt=&quot;A minimalist graphic with the phrase “MCP is a USB port, not a hard drive” in bold black text on a light blue gradient background. Below the text are two black icons: a USB port on the left and a hard drive on the right, separated by a “not equal to” (≠) symbol, emphasizing the conceptual difference.&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;what-mcp-actually-is&quot;&gt;What MCP Actually Is&lt;/h2&gt;

&lt;p&gt;MCP is a &lt;strong&gt;protocol&lt;/strong&gt;:
a JSON-RPC 2.0 contract over stdio or WebSocket that lets an LLM client discover and call “tools”
exposed by separate processes (“servers”).
That’s it.
It’s a clean, low-level “USB port” for your LLM to understand and direct your agent software process
to talk to something else—files, databases, ticketing systems, whatever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What MCP gives you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A standard handshake to list tools, resources, and prompts&lt;/li&gt;
  &lt;li&gt;A way to call those tools and stream back results, logs, and progress&lt;/li&gt;
  &lt;li&gt;A boundary between the model client and the side-effectful world (permissions, isolation, auditing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What MCP does &lt;em&gt;not&lt;/em&gt; give you:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Indexing, caching, or search&lt;/li&gt;
  &lt;li&gt;Pagination or resumable jobs semantics&lt;/li&gt;
  &lt;li&gt;Faster upstream APIs&lt;/li&gt;
  &lt;li&gt;Infinite context windows (or cheap ones)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s closer to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetch()&lt;/code&gt; than it is to “Snowflake + Airflow + dbt”.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;where-the-pain-actually-lives&quot;&gt;Where the Pain Actually Lives&lt;/h2&gt;

&lt;p&gt;When someone asks for scraping “all the links since the beginning of time,” MCP is just the messenger.
The real bottlenecks are:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Source API limits&lt;/strong&gt;
Slack, Jira, Confluence, you name it—these APIs paginate, throttle, and often cap historical access.
MCP can’t negotiate you a higher rate limit with downstream services.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Network &amp;amp; serialization overhead&lt;/strong&gt;
Every page of 200 messages gets marshaled into JSON, shipped over the wire, unmarshaled,
and then—if you’re unlucky—stuffed into the model’s context. That’s a lot of busywork for everyone involved.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Token budgets&lt;/strong&gt;
“Just dump it into the model” is how you burn through a 200k context window in one shot.
You’ll end up chunking and summarizing anyway.
Better to design for that from the start.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Lack of long-running job semantics&lt;/strong&gt;
MCP doesn’t define a job queue, checkpoints, or retry policies.
If you need to crawl 8 years of chat logs, do it like a grown-up:
in a background job with state, not an interactive REPL loop with a model.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;a-rubric-good-vs-bad-fits-for-mcp&quot;&gt;A Rubric: Good vs. Bad Fits for MCP&lt;/h2&gt;

&lt;h3 id=&quot;great-uses&quot;&gt;Great Uses&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Small, deterministic calls&lt;/strong&gt;: “Create a Jira ticket with these fields.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scoped reads&lt;/strong&gt;: “Grab and summarize the last 50 messages from the #design channel.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Controlled writes&lt;/strong&gt;: “Rotate this credential,” “Run this Terraform plan (and show me the diff).”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Thin indexing facades&lt;/strong&gt;: “Search the issue index for ‘SPIR-V error’ (the heavy lifting was precomputed elsewhere).”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;terrible-uses&quot;&gt;Terrible Uses&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Bulk export / ETL&lt;/strong&gt;: “Give me every Confluence page created since 2015 and summarize.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Analytics scans&lt;/strong&gt;: “Compute the average time-to-close for 400k tickets.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Multi-GB artifacts&lt;/strong&gt;: “Send the full build logs for the last 10,000 pipelines.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Anything that needs orchestration&lt;/strong&gt;: map/reduce, long-running jobs, backoff retries, partial failure handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a human engineer would script it to run overnight with a cron job and some careful retry logic,
don’t expect MCP + a chat loop to brute-force it interactively.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;patterns-that-actually-work&quot;&gt;Patterns That Actually Work&lt;/h2&gt;

&lt;p&gt;When you &lt;em&gt;do&lt;/em&gt; need big-ish data or repeated access, pattern your MCP servers like this:&lt;/p&gt;

&lt;h3 id=&quot;1-precompute-and-index-elsewhere&quot;&gt;1. &lt;strong&gt;Precompute and Index Elsewhere&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Nightly ETL Slack → S3 → Snowflake (or a vector store).
Then expose a &lt;strong&gt;search&lt;/strong&gt; tool over that index through MCP.
The model sees a fast query surface—not the raw firehose.&lt;/p&gt;

&lt;h3 id=&quot;2-server-side-summaries--pagination-links&quot;&gt;2. &lt;strong&gt;Server-side Summaries &amp;amp; Pagination Links&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Have your tool return: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{ summary: &quot;...&quot;, next_cursor: &quot;abc123&quot; }&lt;/code&gt; instead of 10,000 raw rows.
Let the model decide if it needs page 2.&lt;/p&gt;

&lt;h3 id=&quot;3-background-jobs--job-ids&quot;&gt;3. &lt;strong&gt;Background Jobs + Job IDs&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Expose a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_export&lt;/code&gt; tool that kicks off a real background job (queue, cron, whatever) and returns a job ID.
Provide &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_status(job_id)&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_results(job_id)&lt;/code&gt; tools.
MCP stays snappy; heavy lifting lives elsewhere.&lt;/p&gt;

&lt;h3 id=&quot;4-advertise-limits-up-front&quot;&gt;4. &lt;strong&gt;Advertise Limits Up Front&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;During capability discovery, include metadata: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_rows&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_bytes&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max_duration&lt;/code&gt;.
If the model knows it only gets 1,000 rows at a time, it can plan a more efficient strategy.&lt;/p&gt;

&lt;h3 id=&quot;5-shard-large-tasks&quot;&gt;5. &lt;strong&gt;Shard Large Tasks&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;Instead of “export all messages”, expose tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_channel_links(channel_id, start_ts, end_ts, limit)&lt;/code&gt;.
The model can iterate by month or by cursor token, not by “hope and pray.”&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;snappy-lines-for-slide-decks--exec-briefings&quot;&gt;Snappy Lines for Slide Decks &amp;amp; Exec Briefings&lt;/h2&gt;

&lt;p&gt;Feel free to steal these:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;“MCP doesn’t turn rate‑limited APIs into data lakes.”&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“It’s a function-call adapter, not a data pipeline.”&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Your throughput is min(slowest upstream API, model token budget).”&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“If you wouldn’t do it in a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; call, don’t try to do it in a single MCP call.”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;a-tiny-visual&quot;&gt;A Tiny Visual&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;User → LLM Client ──(MCP)──&amp;gt; Tool Server ──&amp;gt; Real System (Slack/Jira/DB)
               ^              ^
           Protocol        All the speed, limits,
                            and pain live here
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;so-how-do-i-pitch-mcp-without-the-hype&quot;&gt;So… How Do I Pitch MCP Without the Hype?&lt;/h2&gt;

&lt;p&gt;Try this analogy:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;“MCP is a USB port. It standardizes the plug.&lt;/strong&gt;
&lt;strong&gt;It doesn’t make the disk spin faster, increase its capacity, or magically reorganize your files.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then follow with a practical recipe:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Use MCP to wire the model into well-scoped actions.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Do heavy data work outside the loop.&lt;/strong&gt; Precompute, cache, index.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Expose smart, limited endpoints that a model can compose.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Instrument and log everything.&lt;/strong&gt; (You’ll need to debug the LLM’s tool usage.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finish with a reality check:
“Yes, we can hook Claude up to Slack.
No, it will not be your compliance archive or analytics warehouse.
Different problems, different tools.”&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;closing-thought&quot;&gt;Closing Thought&lt;/h2&gt;

&lt;p&gt;Protocols are boring on purpose. That’s their value.
MCP gives us a boring, predictable way for models to poke the world.
Treat it like a stable connector—and build the real data plumbing where it belongs.
That’s how you get something reliable &lt;em&gt;and&lt;/em&gt; fast, without selling fantasy bandwidth to your stakeholders.&lt;/p&gt;
</content:encoded>
        <pubDate>Sun, 03 Aug 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/08/mcp-is-a-usb-port-not-a-hard-drive/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/08/mcp-is-a-usb-port-not-a-hard-drive/</guid>
        
        <category>ai</category>
        
        <category>mcp</category>
        
        
      </item>
    
      <item>
        <title>Your Information Diet in the Age of AI</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>I was on a call with a colleague, walking through some Wardley Maps I&apos;d drawn for various AI tools. Partway through my overview, he stopped me and asked a question I&apos;ve heard a couple of times before: &quot;How do you find out about these things?&quot; I paused, realizing that the answer has less to do with any magic trick and more to do with habits— essentially, what I choose to read, watch, and listen to on a daily basis. In other words, it comes down to my information diet.
</description>
        <content:encoded>&lt;p&gt;I was on a call with a colleague,
walking through some &lt;strong&gt;&lt;a href=&quot;https://learnwardleymapping.com/book/&quot;&gt;Wardley Maps&lt;/a&gt;&lt;/strong&gt; I’d drawn for various AI tools.
Partway through my overview, he stopped me and asked a question I’ve heard a couple of times before:
“&lt;strong&gt;How do you find out about these things?&lt;/strong&gt;”
I paused, realizing that the answer has less to do with any magic trick
and more to do with habits—essentially,
what I choose to read, watch, and listen to on a daily basis.
In other words, it comes down to my &lt;strong&gt;information diet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We live in an age where AI can answer questions, recommend content, and even generate entire articles for us.
You might lean on these AI tools for quick answers,
but at the end of the day &lt;strong&gt;you are still the one reading, watching, and listening&lt;/strong&gt; to information sources.
The critical question is: are those sources high quality?
Or are you unwittingly on the receiving end of the &lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Gish_gallop&quot;&gt;&lt;em&gt;Gish gallop&lt;/em&gt;&lt;/a&gt;&lt;/strong&gt;?
If you’re not familiar with that term,
the &lt;strong&gt;Gish gallop&lt;/strong&gt; is a rhetorical technique where someone overwhelms you with a barrage of arguments
(often of dubious quality) with no regard for accuracy.
In an era of infinite content—much of it auto-generated or spammy—it’s
dangerously easy to get galloped by misinformation or superficial noise.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/your-information-diet-in-the-age-of-ai.png&quot; alt=&quot;A stylized illustration of an &apos;Information Diet&apos; pyramid. The pyramid is divided into four layers from bottom to top: books, speech bubbles, and newspapers at the base; social media icons and short-form content in the middle; the Twitter bird icon in the upper tier; and a microchip labeled &apos;AI&apos; at the top. The design uses muted shades of beige, blue, and orange to convey different types of information sources.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;the-importance-of-a-healthy-information-diet&quot;&gt;The Importance of a Healthy Information Diet&lt;/h2&gt;

&lt;p&gt;Just like your body needs a balanced diet of nutritious food, your mind needs a balanced intake of information.
The core message I want to share is that for your day-to-day work and life,
it’s vital to cultivate a &lt;strong&gt;healthy information diet&lt;/strong&gt;—especially
if you work in tech or any fast-moving field like AI.
Even if you don’t work directly on AI, there’s a good chance you’re using it indirectly.
If you’re not consciously mixing the right information into your brain and applying it to your day-to-day decisions,
you risk stagnating in your approach.
In the tech world, things change quickly and &lt;strong&gt;the moment you stop learning is the moment you start
&lt;a href=&quot;https://www.youtube.com/watch?v=6xG4oFny2Pk&quot;&gt;losing your edge&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think of it this way: you are what you eat, and by the same token
“&lt;strong&gt;&lt;a href=&quot;https://health.economictimes.indiatimes.com/news/health-it/ai-search-answers-are-the-fast-food-of-your-information-diet-convenient-and-tasty-but-no-substitute-for-good-nutrition/110868159#:~:text=Perhaps%20you%20can%27t%20afford%20to,you%20are%20how%20you%20search&quot;&gt;you are how you search&lt;/a&gt;&lt;/strong&gt;.”
Feeding solely on easy, processed info—the equivalent of junk food—can leave your knowledge malnourished.
Sure, an AI-generated summary or the top Google result might be a quick fix for a simple question,
or a Yahoo News headline might leave you feeling like you’ve gotten a real picture of some type of new advancement
in some space,
but be cautious about making that your main sustenance.
One information scientist aptly compared
&lt;strong&gt;&lt;a href=&quot;https://health.economictimes.indiatimes.com/news/health-it/ai-search-answers-are-the-fast-food-of-your-information-diet-convenient-and-tasty-but-no-substitute-for-good-nutrition/110868159#:~:text=AI%20Overviews%20is%20like%20fast,longer%20and%20may%20cost%20more&quot;&gt;AI’s instant answers to fast food&lt;/a&gt;&lt;/strong&gt;:
convenient and tasty, but not the healthiest choice in the long run.
But this goes beyond AI and into your everyday information intake habits:
If you only consume bite-sized, context-free tidbits
(think of those sensational one-line news alerts or random social media takes),
you’re likely missing the vitamins and fiber of deeper understanding.
A healthy information diet means balancing those quick bites with more substantive meals—the
reports, articles, books, and conversations that challenge and grow your thinking,
and add to your repertoire of skills.&lt;/p&gt;

&lt;h2 id=&quot;quality-over-quantity-beware-the-flood-of-junk-info&quot;&gt;Quality Over Quantity: Beware the Flood of Junk Info&lt;/h2&gt;

&lt;p&gt;Having a healthy info diet is as much about &lt;strong&gt;avoiding bad info&lt;/strong&gt; as it is about finding good info.
The term “Gish gallop” I mentioned is a warning sign:
when you notice you’re being inundated with a &lt;strong&gt;firehose of arguments or facts of questionable quality&lt;/strong&gt;,
step back and assess.
This can happen when scrolling through algorithm-driven feeds that prioritize engagement over accuracy—you
get spammed with clickbait, outrage, and half-truths all mixed together.
It can even happen when using AI tools if you treat their output as gospel.
AI might confidently present information that is &lt;strong&gt;incorrect&lt;/strong&gt;—but
I would argue there’s a worse scenario than blatantly incorrect info (which can be fact-checked),
and this is when you’re getting answers and information that is woefully &lt;strong&gt;oversimplified&lt;/strong&gt;.
(Remember, these models predict plausible answers; they don’t guarantee truthful ones,
or further ones with all the necessary context for you to understand a topic,
&lt;em&gt;unless&lt;/em&gt; you ask the right questions—more on that later…).
If you rely on these without critique, you might end up with a head full of fluff or outright falsehoods,
which is the intellectual equivalent of subsisting on cotton candy.&lt;/p&gt;

&lt;p&gt;So how do you ensure quality?
One way is to slow down and &lt;strong&gt;cross-check information&lt;/strong&gt; instead of accepting the first answer.
It might take a bit more work (like reading a full article or checking multiple sources),
but doing so gives you the ability to compare evidence and form your own judgment.
Think of consuming information like eating:
grabbing fast food on the run (a quick AI answer or a single Twitter thread) is fine once in a while,
but you don’t want every meal to be from the drive-through.
&lt;strong&gt;&lt;a href=&quot;https://health.economictimes.indiatimes.com/news/health-it/ai-search-answers-are-the-fast-food-of-your-information-diet-convenient-and-tasty-but-no-substitute-for-good-nutrition/110868159#:~:text=AI%20Overviews%20is%20like%20fast,longer%20and%20may%20cost%20more&quot;&gt;AI-generated overviews are like drive-through burgers—quick and hot, but not very nutritious&lt;/a&gt;&lt;/strong&gt;.
Taking the time to read in-depth sources or multiple perspectives is more like cooking a balanced meal at home:
it’s slower and requires effort, but you know exactly what you’re getting.
This extra effort
“&lt;strong&gt;&lt;a href=&quot;https://health.economictimes.indiatimes.com/news/health-it/ai-search-answers-are-the-fast-food-of-your-information-diet-convenient-and-tasty-but-no-substitute-for-good-nutrition/110868159#:~:text=take%20a%20little%20work%2C%20but,for%20learning%2C%20discovery%20and%20serendipity&quot;&gt;gives you back the ability to examine multiple sources… and leaves open the possibilities for learning, discovery and serendipity&lt;/a&gt;&lt;/strong&gt;”
that a one-and-done info snippet would never provide.&lt;/p&gt;

&lt;p&gt;In short, be vigilant about info quality.
If something smells fishy or too hypey, question it.
Develop a bit of a BS filter.
Over time, you’ll get better at distinguishing a well-researched piece from a regurgitated press release,
or a genuine expert opinion from a performative rant.
(Oh hey, maybe this blog post is a performative rant—check and question your sources!)
High-quality inputs lead to high-quality outputs in your own work and decisions—&lt;em&gt;garbage in, garbage out&lt;/em&gt;,
as the saying goes…&lt;/p&gt;

&lt;h2 id=&quot;curate-what-you-consume&quot;&gt;Curate What You Consume&lt;/h2&gt;

&lt;p&gt;Since there’s an infinite amount of content out there, a key skill is &lt;strong&gt;curation&lt;/strong&gt;—deliberately
choosing what to read/watch/listen to.
Don’t passively scroll whatever the algorithm shoves at you.
Take control of the menu.
For me, a big part of curation is on social platforms.
(I miss old school Twitter for this.)
Over the years, I’ve &lt;strong&gt;sought out credible voices&lt;/strong&gt; in the tech industry
and followed them to essentially build a custom “feed” of quality insights.
Folks like Camille Fournier, Will Larson, Michael Lopp, Kelsey Hightower, Nicole Forsgren, Bryan Liles, Gergely Orosz,
Laura Tacho, Abi Noda, Sam Schillace (to name &lt;em&gt;just a few&lt;/em&gt;)
consistently share valuable perspectives on engineering leadership and emerging technology.
By following leaders of this caliber, I ensure that my timeline isn’t just the outrage-of-the-day or viral memes,
but rather snippets of wisdom, links to great articles, and heads-up on new tools worth looking at.
In other words, I let &lt;strong&gt;respected experts and practitioners&lt;/strong&gt; populate my brain’s newsfeed,
not random influencers or click-chasers.&lt;/p&gt;

&lt;p&gt;Social media can be a double-edged sword.
It’s where a lot of breaking info and niche discussions happen first, but it’s also a source of endless junk.
The trick is aggressively filtering who you follow.
(Sidebar: this is a great trick to use with Slack/Teams channels and email in your inbox as well—aggressively
mute/leave channels to reduce noise,
organize them with something like the &lt;strong&gt;&lt;a href=&quot;https://fortelabs.com/blog/para/&quot;&gt;PARA&lt;/a&gt;&lt;/strong&gt; method,
and follow some tips on
&lt;strong&gt;&lt;a href=&quot;https://www.hanselman.com/blog/the-three-most-important-outlook-rules-for-processing-mail&quot;&gt;how to get control of your inbox&lt;/a&gt;&lt;/strong&gt;—I
learned all these things from people I follow…)
If someone consistently adds value (teaches you something, makes you think, points you to good resources), keep them.
If they mostly spout hot takes or amplify noise, mute or unfollow.
&lt;strong&gt;The information you consume is just as important as the food you eat&lt;/strong&gt;—doomscrolling
Twitter aimlessly is the mental equivalent of chowing down on candy bars all day.
I try to limit the empty calories and focus on accounts and publications that are more like a balanced meal.&lt;/p&gt;

&lt;p&gt;Curating your information diet goes beyond just who you follow on Twitter.
It also means picking good &lt;strong&gt;long-form sources&lt;/strong&gt; to balance out the short-form stream.
For example, I subscribe to a few weekly newsletters (like Gergely Orosz’s &lt;em&gt;Pragmatic Engineer&lt;/em&gt;)
and industry blogs that summarize what’s new in tech.
These act like a hearty weekly dinner—more substantial and organized than the constant snacking of social media.
I also make time for books and deep-dive articles when I can,
because often the &lt;strong&gt;best insights come from longer, more structured content&lt;/strong&gt;
that an author spent months or years refining.
(Only the best ideas tend to survive the gauntlet of writing a full book or peer-reviewed paper,
so those can be high nutritional value, so to speak.)
By reading books or listening to hour-long podcast interviews,
you force yourself to engage with ideas at a deeper level than a tweetstorm or headline can offer.
It’s like the difference between a full-course meal and a quick bag of chips.&lt;/p&gt;

&lt;p&gt;Finally, remember that your &lt;strong&gt;peers and community&lt;/strong&gt; are part of your information diet too.
Find the people in your network who are always tinkering, always learning, always sharing cool stuff they discovered.
Every office or friend group has a few of these “information chefs” who love to cook up new ideas.
Take advantage of that—ask them what they’re excited about lately.
If you’re active in any Slack/Discord groups, forums, or local meetups,
pay attention to what those trusted folks are buzzing about.
Often I learn about the next big library, or a handy AI tool,
from an engineer in my team or a friend in the industry who says “Hey, have you seen this yet?”
Those conversations can be golden.
They’re like getting a recommendation for a great new restaurant from a friend: personalized and usually reliable.&lt;/p&gt;

&lt;h2 id=&quot;keep-your-mind-open-to-new-ideas&quot;&gt;Keep Your Mind Open to New Ideas&lt;/h2&gt;

&lt;p&gt;If you only consume the same type of information over and over, you’re going to get the same results.
A healthy diet has variety—some new flavors and cuisines in addition to your comfort food.
Likewise, a healthy &lt;strong&gt;information diet means embracing things that are unfamiliar or even initially uncomfortable.&lt;/strong&gt;
Human nature being what it is, most new ideas and technologies get some pushback at first.
Maybe it’s skepticism (“That’ll never work here”),
maybe it’s discomfort (“This is too different from what I know”),
or plain inertia (“I don’t have time to learn that”).
But if you dismiss every new development out of hand, you’ll wake up one day stuck with a very outdated worldview.&lt;/p&gt;

&lt;p&gt;I encourage you to &lt;strong&gt;push beyond the initial pushback&lt;/strong&gt; and keep going a little further than feels natural.
Often, once you get past that first hill of resistance, you start seeing the value on the other side.
We’ve all seen colleagues who refused to learn the new tool or ignored the new trend;
a year or two later, they find themselves playing catch-up.
Don’t let that be you.
The tech landscape moves fast.
“&lt;strong&gt;&lt;a href=&quot;https://mccricardo.com/engineering-evolution-the-antidote-to-career-stagnation/#:~:text=Stagnation%2C%20the%20antithesis%20of%20growth%2C,moment%20you%20start%20losing%20value&quot;&gt;If you’re not moving forward, you’re falling behind&lt;/a&gt;&lt;/strong&gt;,”
as one engineer wrote about avoiding stagnation.
We see this over and over:
cloud computing, DevOps, containerization, serverless, machine learning—pick
any tech trend, and early on there were plenty of naysayers saying it’s all hype or not worth the effort.
But those who took the time to explore new approaches often reaped huge benefits,
while the skeptics were left scrambling to adapt once the change became inevitable.
A very current example is AI-assisted development.
When GPT-based tools first emerged, a lot of folks (understandably) pushed back:
“Can we trust the code it writes?”,
“Won’t this take our jobs?”, etc.
Yet here we are a short time later, and such tools have gone mainstream.
In fact,
&lt;strong&gt;&lt;a href=&quot;https://www.geekwire.com/2024/microsoft-study-finds-75-of-knowledge-workers-using-ai-at-work-nearly-doubling-in-six-months/#:~:text=Among%20the%20findings%20this%20year%3A,research%20conducted%20with%20big%20companies&quot;&gt;three-quarters of workers are now using AI in their jobs&lt;/a&gt;&lt;/strong&gt;,
one way or another
(if this &lt;strong&gt;&lt;a href=&quot;https://news.microsoft.com/annual-wti-2024/&quot;&gt;Microsoft study&lt;/a&gt;&lt;/strong&gt; referenced in that prior link
is to be believed—again, check and question your sources!).
The initial fears and objections are giving way to “Okay, how do we actually leverage this properly?”
Those who kept an open mind early on are now ahead of the curve in using AI to be more productive.
The point is, don’t reject a new idea just because it’s new. Sample it.
Even if it ends up not to your taste, at least you made an informed decision.
And if it does turn out to be the next big thing, you’ll be glad you got a head start.&lt;/p&gt;

&lt;p&gt;Keeping an open mind also means sometimes venturing outside your usual bubble of information.
If you’re a software engineer, it can be useful to occasionally read something from, say,
a design blog or a product management podcast,
just to see how adjacent fields are thinking.
If you’re deep into AI, it might pay to read what skeptics or ethicists are saying about it,
not just the cheerleading press releases.
A diverse information diet ensures you’re not lopsided in your understanding.
It inoculates you against groupthink.
Sure, most of the time I read my preferred tech sources,
but every now and then I deliberately read an opposing viewpoint or an analysis from a different domain.
Despite working in AI, I recently read
&lt;strong&gt;&lt;a href=&quot;https://thecon.ai/&quot;&gt;&lt;em&gt;The AI Con: How to Fight Big Tech’s Hype and Create the Future We Want&lt;/em&gt;&lt;/a&gt;&lt;/strong&gt;.
Did I agree with 100% of that book? No.
But much of it did resonate with me,
and it has helped me to hone my BS filter when I see some new, over-hyped headline
about something that’s going to “change everything.”
Forcing yourself to read things that run counter to your world view or challenge the prevailing mainstream media hype
is a bit like eating your vegetables—not always the most immediately fun part,
but it makes you stronger in the long run.
(And steering away from AI hype for a second,
if you want some critical thinking and de-hyping of quantum computing,
especially as it relates to un-founded fears about it “breaking cryptography,”
take a look at &lt;strong&gt;&lt;a href=&quot;https://eprint.iacr.org/2025/1237.pdf&quot;&gt;this paper&lt;/a&gt;&lt;/strong&gt; which is dense and mathy,
but basically dissects how factorisation exercises in quantum computers have been a sleight-of-hand magic trick,
or watch 30-minutes of &lt;strong&gt;&lt;a href=&quot;https://youtu.be/kNhtonFK_1o?t=889&quot;&gt;this YouTube video at 14:49&lt;/a&gt;&lt;/strong&gt; from Security Now
which explains these quantum factorisation sleight-of-hand tricks in a relatable way.)&lt;/p&gt;

&lt;h2 id=&quot;beyond-the-headlines-choose-trusted-voices&quot;&gt;Beyond the Headlines: Choose Trusted Voices&lt;/h2&gt;

&lt;p&gt;Another aspect of a good information diet is knowing &lt;strong&gt;who (and what) to trust&lt;/strong&gt;,
especially when it comes to news and analysis.
We’ve all seen how a story can get distorted as it’s rehashed across the internet.
A company might release a nuanced research paper, and by the time it makes it to a Yahoo News headline,
it’s been dumbed down to “Scientists Create Monster AI That Will Take Your Job!”
Getting your tech news solely from generic mainstream outlets is like trying to sustain yourself on popcorn—it’s
mostly air, with a bit of salt and butter.
&lt;strong&gt;&lt;a href=&quot;https://health.economictimes.indiatimes.com/news/health-it/ai-search-answers-are-the-fast-food-of-your-information-diet-convenient-and-tasty-but-no-substitute-for-good-nutrition/110868159#:~:text=It%27s%20easy%20to%20fall%20for,for%20food%20and%20for%20information&quot;&gt;It’s easy to fall for sensational headlines and bite-size news that lack context&lt;/a&gt;&lt;/strong&gt;,
because they’re designed to grab attention, not to inform deeply.
So when possible, go beyond the headline.
If something interests you, click through and read the details (or find the original source).
Often the reality is more complex—and more interesting—than the blurb.&lt;/p&gt;

&lt;p&gt;Let’s use a non-tech analogy.
Say a new &lt;em&gt;Ghostbusters&lt;/em&gt; movie comes out.
Who are you going to trust with a review?
Perhaps one of the following:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The Today Show&lt;/strong&gt;: a quick segment on morning TV with a cheerful host reading talking points.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Rotten Tomatoes&lt;/strong&gt;: an aggregation of many critics’ and viewers’ opinions, averaged into a score.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A like-minded friend who already saw it&lt;/strong&gt;: someone whose taste in movies usually matches yours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For me, the choice is obvious: I’d call up the friend (or read their texts about it).
Why? Because I know &lt;em&gt;how&lt;/em&gt; they think and I trust their perspective.
&lt;em&gt;The Today Show&lt;/em&gt; might give a superficial take aimed at a broad audience
(“It’s a fun summer romp for the family!”)—that’s like a headline with no depth.
Rotten Tomatoes is more informative, but it’s an impersonal crowd score.
My friend, on the other hand, will tell me candidly,
“Yeah, it’s a decent throwback but the plot was weak,
I know you hate those cheesy jokes so you’ll probably find it mediocre.”
That’s actionable intel for me.&lt;/p&gt;

&lt;p&gt;Translate this back to technology and work.
When you’re evaluating a new programming framework, a new SaaS product, or any new “shiny thing” in tech,
consider the sources of info in a similar vein:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Official marketing or mainstream media&lt;/strong&gt; (equivalent to the &lt;em&gt;Today Show&lt;/em&gt;):
quick soundbites, maybe biased towards positivity
(or in the case of AI doomers, stoking your fears and distracting you from real-world current day problems),
not deeply technical.
Useful for a high-level gist, but not entirely trustworthy for making a decision.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Aggregated opinions&lt;/strong&gt; (equivalent to Rotten Tomatoes):
for example, a Stack Overflow thread, a collection of reviews, a Gartner Magic Quadrant.
These give you a broader sense of consensus or common pros/cons.
Better, but can lack context of your specific needs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Trusted peers or experts who’ve tried it&lt;/strong&gt; (equivalent to your friend):
a blog post by an engineer who implemented the tool in a project,
a conversation with someone at a meetup who has hands-on experience,
or an in-depth YouTube review by a respected techie.
These sources are often the most valuable
because they can tell you the &lt;strong&gt;nuances&lt;/strong&gt;—the “gotchas,”
the unexpected benefits,
how it compares to similar tools, etc.,
all from a point of view you recognize as honest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever possible, &lt;strong&gt;put more weight on the voices that have proven trustworthy and aligned with your context&lt;/strong&gt;.
In my case, if a Camille Fournier or Kelsey Hightower shares an opinion on a new infrastructure tool,
I’m inclined to listen closely.
If randomsocialmediauser123 hypes the same tool with zero track record, I take it with a big grain of salt.
This isn’t to say famous or well-known folks are always right (they aren’t),
but over time you learn which people or outlets tend to have signal and which are just noise.&lt;/p&gt;

&lt;p&gt;And if you don’t have a go-to friend or expert on a topic? Consider becoming that person yourself.
Dive in and experiment so &lt;strong&gt;you&lt;/strong&gt; can share first-hand knowledge.
It’s the principle of “trust but verify” in action:
take in others’ opinions, but validate through your own exploration when you can.
Not only will you get a better understanding,
you’ll also be contributing back to the information ecosystem with your findings—maybe
writing your own blog post or giving your colleagues the real scoop around the proverbial water cooler.&lt;/p&gt;

&lt;h2 id=&quot;final-thoughts-you-are-what-you-read-so-choose-wisely&quot;&gt;Final Thoughts: You Are What You Read (So Choose Wisely)&lt;/h2&gt;

&lt;p&gt;Circling back to that question that started it all—&lt;em&gt;“How do you find out about these things?”&lt;/em&gt;—the
answer boils down to cultivating and maintaining a healthy information diet.
In the age of AI, this is both more challenging and more important than ever.
More challenging, because there’s an overwhelming buffet of content being served up, not all of it healthy or even true.
More important, because the pace of change is blistering;
falling behind can have real consequences for your career and understanding of the world.&lt;/p&gt;

&lt;p&gt;The good news: you are in control of what you consume. Be intentional.
Feed your mind with high-quality information from a variety of sources.
&lt;strong&gt;Balance&lt;/strong&gt; the quick bites of AI-generated answers or social media chatter
with the slower digestion of books, articles, and thoughtful conversations.
&lt;strong&gt;Curate&lt;/strong&gt; who you listen to, so that you’re hearing from people who inspire and educate you,
not just echo chambers that amplify hype.
&lt;strong&gt;Experiment&lt;/strong&gt; and taste new ideas, even if they seem strange at first—you
might discover something game-changing, or at least you’ll expand your palate.
And above all, &lt;strong&gt;stay curious&lt;/strong&gt;.
A curious mind naturally seeks out better information and resists the junk food of shallow content.&lt;/p&gt;

&lt;p&gt;If you do all that, you’ll have an information diet that keeps you sharp, informed, and ready for whatever comes next.
And when a colleague or friend marvels at how you’re always ahead of the curve,
you can smile and say, “I just watch what I consume.”
In the end, the quality of what you put into your brain determines what you get out of it—so choose wisely.
Bon appétit!&lt;/p&gt;
</content:encoded>
        <pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/07/your-information-diet-in-the-age-of-ai/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/07/your-information-diet-in-the-age-of-ai/</guid>
        
        <category>ai</category>
        
        <category>information-diet</category>
        
        <category>gish-gallop</category>
        
        
      </item>
    
      <item>
        <title>The Many Contexts of Model Context Protocol</title>
        <dc:creator>Ryan Spletzer</dc:creator>
        <description>Or: why &quot;where&quot; your MCP server runs matters just as much as &quot;what&quot; it does.
</description>
        <content:encoded>&lt;p&gt;Or: why “where” your MCP server runs matters just as much as “what” it does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; Running an MCP Server on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; is a night-and-day difference compared to running one remotely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local&lt;/strong&gt; == single user, implicit trust, almost zero auth to the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote&lt;/strong&gt; == multi-user, explicit trust, real authentication to the server, security trimming, token exchange, and all
the headaches (and rewards) that come with it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/the-many-contexts-of-model-context-protocol.png&quot; alt=&quot;A digital illustration titled &amp;quot;The Many Contexts of Model Context Protocol&amp;quot; shows a central AI model icon connected by
lines to four surrounding icons: a desktop computer (local context), a cloud server (remote/server context), an office
building (enterprise or multi-tenant context), and a person (user context). Each icon is enclosed in a circle with
colorful backgrounds, symbolizing different deployment and usage contexts of MCP servers.&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;1-introduction--context-in-two-senses&quot;&gt;1. Introduction – Context in Two Senses&lt;/h2&gt;

&lt;p&gt;When people hear &lt;em&gt;&lt;a href=&quot;https://modelcontextprotocol.io/introduction&quot;&gt;Model Context Protocol (MCP)&lt;/a&gt;&lt;/em&gt;, they usually think
about the data context an LLM needs—documents, code, tickets, whatever.&lt;/p&gt;

&lt;p&gt;Less obvious, but just as important, is the &lt;em&gt;deployment&lt;/em&gt; context in which the MCP server itself lives.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Local sandbox&lt;/strong&gt;: One dev, one laptop, one loopback interface.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Remote service&lt;/strong&gt;: Many users, many clients, many downstream systems, and a permanent address on the public internet
(or barring that, at least on your private network).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those two worlds demand radically different security and architecture choices.&lt;/p&gt;

&lt;p&gt;MCP has only existed as a “thing” since late 2024, and has only really gotten popular (and only gotten serious with auth
specs) as of the last couple of months, and the mere ~6 or so months of its existence is part of the reason why the
state of affairs with respect to running these in advanced remote scenarios is relatively immature. (Yes, I said it—for
those on the agentic hype trains, don’t @ me.)&lt;/p&gt;

&lt;h2 id=&quot;2-local-mcp--the-joy-of-single-user-simplicity&quot;&gt;2. Local MCP – The Joy of Single-User Simplicity&lt;/h2&gt;

&lt;p&gt;I am going to pick on the &lt;a href=&quot;https://github.com/github/github-mcp-server&quot;&gt;GitHub MCP Server&lt;/a&gt; as an example, because
despite it being relatively popular—with GitHub being one of the first things developers would likely want to connect to
with agentic tools—it is imbued with all the naïveté I’ve come to expect in the MCP servers that I’ve looked at thus
far, making it effectively a non-starter for sophisticated enterprises who would wish to run it remotely (unless you
applied some serious elbow grease—more on that later).&lt;/p&gt;

&lt;p&gt;Running the GitHub MCP Server locally could look like the following:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Provide your GitHub (preferably Fine-Grained) Personal Access Token&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-rsp&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GitHub PAT: &quot;&lt;/span&gt; GITHUB_PERSONAL_ACCESS_TOKEN

&lt;span class=&quot;c&quot;&gt;# Run the GitHub MCP Server&lt;/span&gt;
docker run &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;GITHUB_PERSONAL_ACCESS_TOKEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$GITHUB_PERSONAL_ACCESS_TOKEN&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  ghcr.io/github/github-mcp-server:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/github-mcp-server-stdio.png&quot; alt=&quot;GitHub MCP Server stdio screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;You’ll notice that last message: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GitHub MCP Server running on stdio&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdio&lt;/code&gt; stands for “Standard Input/Output” and represents an inter-process communication approach to providing an MCP
Server to a local MCP client, like Visual Studio Code.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stdio&lt;/code&gt; only is feasible for local scenarios, so that leads us to the next logical question: how would we make the
GitHub MCP server available over HTTP / SSE (Server-Sent Events) transport, or the even &lt;em&gt;more&lt;/em&gt; recent and modern
&lt;a href=&quot;https://github.com/github/github-mcp-server/issues/2&quot;&gt;Streamable HTTP&lt;/a&gt; transport (which literally was conjured up
alongside the MCP specs in the last few months)?&lt;/p&gt;

&lt;p&gt;The answer to this question illustrates a key point: The GitHub MCP Server
&lt;em&gt;&lt;a href=&quot;https://github.com/github/github-mcp-server/issues/2&quot;&gt;doesn’t support HTTP + SSE&lt;/a&gt;&lt;/em&gt;, and thus doesn’t actually support
being run in a remote setting at all. (Yet.)&lt;/p&gt;

&lt;p&gt;Many of the MCP servers that have been created over the last few months &lt;em&gt;didn’t take into account remote scenarios&lt;/em&gt;, as
they were coded and designed purely with these local clients in mind.&lt;/p&gt;

&lt;p&gt;So let’s turn our attention to an MCP Server that &lt;em&gt;does&lt;/em&gt; have HTTP+SSE transport, the
&lt;a href=&quot;https://github.com/semgrep/mcp&quot;&gt;Semgrep MCP Server&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# There are multiple ways to run this, but to stay consistent we&apos;ll run it with docker:&lt;/span&gt;
docker run &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--rm&lt;/span&gt; ghcr.io/semgrep/mcp &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; sse
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/semgrep-mcp-server-sse.png&quot; alt=&quot;Semgrep MCP Server sse screenshot&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The examples above just show how to &lt;em&gt;run&lt;/em&gt; the servers, but in reality, when consuming via something like Visual Studio
Code, you would wire this up in something like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.vscode/mcp.json&lt;/code&gt; so your IDE is aware of how to start it up when you
open a project.&lt;/p&gt;

&lt;h3 id=&quot;21-why-no-auth-feels-okay&quot;&gt;2.1. Why No Auth Feels Okay&lt;/h3&gt;

&lt;p&gt;You may see this as a developer and think “I don’t get it, this seems totally fine so far”—and you may actually be
correct. See, running these MCP servers locally has certain aspects to it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Physical access is implicit authentication.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;If you can hit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt;, you’re already inside the blast radius (your laptop).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;One tenant = one permission set.&lt;/strong&gt;&lt;/p&gt;

    &lt;p&gt;There’s no question who the user is in this scenario, it’s all just you.&lt;/p&gt;

    &lt;p&gt;The server can safely assume “allow everything.”&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be honest, this is what developers like: developer ergonomics reign supreme when there is zero OAuth dance, no JWT
debugging, no credential managers to deal with. It’s the fastest path from idea to working prototype. But it is these
same ease of use tendencies for working on our local machine that can make us blind to the very real issues of trying to
make these types of MCP servers work in a remote setting. For example, lack of HTTP transport aside, if you attempted to
move the GitHub MCP Server into a remote setting, whose personal access token are you going to use? And if it’s a
privileged personal access token, how do you know the user behind the calling client isn’t accessing repos or other data
that they don’t have access to natively?&lt;/p&gt;

&lt;h3 id=&quot;22-the-boundaries-of-safe-enough&quot;&gt;2.2. The Boundaries of “Safe Enough”&lt;/h3&gt;

&lt;p&gt;The moment you expose that port beyond loopback—say, you port-forward via ngrok so a coworker can demo your
MCP server—&lt;strong&gt;all bets are off&lt;/strong&gt;. Anyone who has the URL owns your MCP, and by that, any permissions you granted to that
GitHub personal access token. That’s fine for a five-minute demo, catastrophic for anything longer.&lt;/p&gt;

&lt;p&gt;If all MCP was is a way to wrap external resources (be they your local file system, or remote database or REST API,
etc.) with your own local MCP servers to provide it to your own local agent tools, then there would be pretty much zero
issues here, and no need for this blog post—it would just be developers doing their thing on their local dev machines.
But the issue with MCP right now is it is trying to be &lt;em&gt;more&lt;/em&gt; than that, and this is where pretty much any off-the-shelf
MCP server is going to struggle in a remote setting.&lt;/p&gt;

&lt;p&gt;In a very real way, &lt;em&gt;many available MCP servers today are at about the same level as a “Hello World” local web app or
API&lt;/em&gt; running on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; with no authentication (and without auth, further you really have no concept of multiple
users or multi-tenancy)—if you’ve created something like that, I would hope that you would understand intuitively that
it in no way could just simply go to production as-is.&lt;/p&gt;

&lt;h2 id=&quot;3-remote-mcp--welcome-to-multi-user-reality&quot;&gt;3. Remote MCP – Welcome to Multi-User Reality&lt;/h2&gt;

&lt;p&gt;Move the same MCP server to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;https://mcp.acme.dev&lt;/code&gt; and everything changes.&lt;/p&gt;

&lt;h3 id=&quot;31-authorization-is-now-mandatory&quot;&gt;3.1. Authorization is Now Mandatory&lt;/h3&gt;

&lt;p&gt;Every request must identify who is calling. The mainstream way is &lt;strong&gt;OAuth / OpenID Connect (OIDC)&lt;/strong&gt;, and the MCP specs
themselves &lt;a href=&quot;https://modelcontextprotocol.io/specification/draft/basic/authorization&quot;&gt;call this out as “optional”&lt;/a&gt;, but
something that SHOULD be implemented for HTTP transport scenarios—in reality, authorization was an afterthought added to
the spec after many individuals and companies pointed out the issues of not defining a way to handle authorization,
since not only would it likely lead to naïve security hazards, but would hurt the interoperability and discoverability
aspects of the ecosystem.&lt;/p&gt;

&lt;p&gt;A call to an MCP server with authorization would look something like the following with a JWT access_token passed in
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; HTTP header with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Bearer&lt;/code&gt; scheme:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;POST /query
Authorization: Bearer eyJhbGciOiJSUzI1N...
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Validates the JWT access_token’s signature against your identity provider’s (IdP’s) &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jwks_uri&lt;/code&gt; which provides the
public keys and is discoverable via the OpenID Connect metadata endpoint.&lt;/li&gt;
  &lt;li&gt;Rejects expired JWT access_tokens (concretely, ensuring the current date time is not past the  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exp&lt;/code&gt; claim in the token,
represented Unix epoch time).&lt;/li&gt;
  &lt;li&gt;Checks that the JWT access_tokens &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aud&lt;/code&gt; claim is in fact the string you expect that represents your MCP resource, not
an arbitrary API. (The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aud&lt;/code&gt; is often represented as a GUID, or some type of unique identifier for your target
resource.)&lt;/li&gt;
  &lt;li&gt;In cases where there is a human user involved, it &lt;em&gt;should&lt;/em&gt; utilize something like the user’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sub&lt;/code&gt; (subject) claim
from the JWT access_token, to understand who the user is for filtering for downstream calls. (Which, no server I’ve
ever seen so far does this—the validation typically &lt;em&gt;stops&lt;/em&gt; with only the checks above, if you’re lucky that they’ve
done all of them right and not missed something. Beyond this authorization check, they typically don’t implement
further fine-grained authorization logic for security trimming, because frankly that can get enterprise-specific and
is difficult to assume or make configurable for a wide variety of scenarios.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reverse proxy (→ Envoy, Traefik) &lt;em&gt;can&lt;/em&gt; handle this so your app code can assume a verified user context—but again,
you may &lt;em&gt;need that user context&lt;/em&gt; in your actual MCP server code to make further authorization determinations. Most
off-the-shelf MCP servers do not—and often cannot—make those authorization decisions for you.&lt;/p&gt;

&lt;p&gt;Note that we just say “Authorization” here and not “Authentication”—that is because technically the authentication
portion happens between the client and the identity provider (IdP) you’re talking to, like Okta or Microsoft Entra ID or
a consumer third party identity provider like Google or Apple or Microsoft.&lt;/p&gt;

&lt;h3 id=&quot;32-authorization--security-trimming&quot;&gt;3.2. Authorization &amp;amp; Security Trimming&lt;/h3&gt;

&lt;p&gt;Authentication (AuthN) only says “Alice is Alice.” Authorization (AuthZ) answers “Which docs/repos/items/objects can
Alice actually read?”&lt;/p&gt;

&lt;p&gt;An enterprise MCP ecosystem could often fan out to services like GitHub, Jira, Confluence, Artifactory, Microsoft Graph,
or a vector DB keyed by user or security group ID’s. That means:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Row-level filters&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SELECT … WHERE user_id = &amp;lt;aliceUserId&amp;gt; OR group_id = &amp;lt;securityGroupIdAliceIsIn&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Search ACLs&lt;/strong&gt;: e.g. Elasticsearch or OpenSearch query-time filters&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Downstream&lt;/strong&gt;: &lt;a href=&quot;https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow&quot;&gt;on-behalf-of (OBO)&lt;/a&gt;
flows; exchange Alice’s inbound JWT access_token scoped for the MCP server resource with your IdP for another JWT
access_token to access an API that accepts user-scoped tokens from your IdP for purposes of security trimming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fail to trim, and your nice AI agent becomes a data-leak vending machine.&lt;/p&gt;

&lt;h3 id=&quot;33-multi-tenant-isolation&quot;&gt;3.3. Multi-Tenant Isolation&lt;/h3&gt;

&lt;p&gt;Thus far we have approached local and remote scenarios only from the standpoint of a developer who is using local tools
to connect to local and remote MCP servers.&lt;/p&gt;

&lt;p&gt;But MCP servers are not destined just for developer tools—they can and will be used in scenarios like enterprise chatbot
assistants, where a lot of the same remote lessons apply with regards to user identity and security trimming, etc.&lt;/p&gt;

&lt;p&gt;But what about when you’re running a multi-tenant SaaS product that has agentic capabilities that wish to consume MCP
Servers?&lt;/p&gt;

&lt;p&gt;You are now storing embeddings, indexes, caches, and data for many customers. Everything needs a partition key.
Consider:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$TENANT_ID:$DOC_ID&lt;/code&gt; keys in Redis.&lt;/li&gt;
  &lt;li&gt;Distinct Milvus/Mongo/Postgres collections/tables/databases per tenant.&lt;/li&gt;
  &lt;li&gt;S3 prefix per tenant (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;s3://mcp-prod/tenants/$id/&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Database filters based on tenant ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it comes to running products, you are now firmly &lt;em&gt;out of the realm of off-the-shelf MCP servers&lt;/em&gt; and now in a place
where you are creating them from scratch—sure, you can derive some “inspo” from some of the MCP servers out there, but
what you’re creating inherently now has to be catered to your products and SaaS ecosystem.&lt;/p&gt;

&lt;p&gt;These considerations even extend to scenarios &lt;em&gt;beyond&lt;/em&gt; your core products and into tools your customers may be using,
like Microsoft 365 Copilot, where they may want to connect their own enterprise Copilot chat to MCP Servers that wrap
various APIs of your products, and you have to provide facilities for users at your customer’s enterprise to
authenticate to provide proper security trimming, in much the same way that you authenticate to any one of a number of
Slack apps as a user to connect to various tools.&lt;/p&gt;

&lt;h2 id=&quot;4-consumer-patterns-drive-security-posture&quot;&gt;4. Consumer Patterns Drive Security Posture&lt;/h2&gt;

&lt;p&gt;This table gives a brief breakdown of some of the remote authentication patterns for various scenarios.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Pattern&lt;/th&gt;
      &lt;th&gt;Who calls MCP?&lt;/th&gt;
      &lt;th&gt;Typical Auth Flow&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;IDE plugin (Visual Studio Code, JetBrains)&lt;/td&gt;
      &lt;td&gt;The developer’s desktop&lt;/td&gt;
      &lt;td&gt;Often local MCP → no auth. If remote: OAuth device flow (perhaps with PKCE); store refresh token in keychain.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Web/Mobile app feature (“Summarize this ticket”)&lt;/td&gt;
      &lt;td&gt;Product backend&lt;/td&gt;
      &lt;td&gt;Backend → MCP: service credential (client cred). User context propagated as JWT or user ID header.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Pure unattended (nightly batch, scheduled agents)&lt;/td&gt;
      &lt;td&gt;Job runner / microservice&lt;/td&gt;
      &lt;td&gt;Service-to-service token only. User context often absent or deferred via signed job token.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The security envelope expands as soon as humans &lt;em&gt;other than the owner&lt;/em&gt; of the target resources are involved.&lt;/p&gt;

&lt;p&gt;When it comes to local and remote MCP servers in an enterprise setting, the reality is you will wind up forking or
wrapping off-the-shelf MCP servers to accommodate your authentication/authorization needs.&lt;/p&gt;

&lt;p&gt;But when it comes to building SaaS products, it gets a little bit more involved, and warrants some more ideation and
discussion.&lt;/p&gt;

&lt;p&gt;Not much of what is described in this blog post is unique to MCP—you could easily replace “MCP Server” with “Rest
API” and most of these considerations still apply—it’s simply that we are &lt;em&gt;re-learning these lessons&lt;/em&gt; in the realm of
MCP as the spec and implementations and thought leadership evolves around it.&lt;/p&gt;

&lt;h2 id=&quot;5-propagating-identity--dual-tokens--obo&quot;&gt;5. Propagating Identity – Dual Tokens &amp;amp; OBO&lt;/h2&gt;

&lt;p&gt;It is worth digging into a hypothetical example of a real world SaaS product to illustrate the inherent unavoidable
complexity involved in properly propagating user context through a call chain from a frontend through a backend and
through a set of collaborating services, which in this case includes an MCP server.&lt;/p&gt;

&lt;p&gt;Imagine your MCP server receives a call from your SaaS product backend. In an &lt;em&gt;ideal&lt;/em&gt; world (which we are so often very
far away from), that backend authorizes itself in with its own token issued through client credentials grant flow, &lt;em&gt;and&lt;/em&gt;
forwards the end-user’s JWT access_token, so the MCP server can do user-level validation and security trimming.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/mcp-product-obo-sequence-diagram.svg&quot; alt=&quot;Sequence Diagram of an on-behalf-of / token exchange flow&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Key points:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Two tokens travel together (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization: Bearer &amp;lt;serviceJWT&amp;gt;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x-user-token: Bearer &amp;lt;userJWT&amp;gt;&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;MCP validates &lt;em&gt;both&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;If CalendarAPI trusts your IdP, MCP can perform an &lt;strong&gt;On-Behalf-Of&lt;/strong&gt; exchange to get a new access token valid for
CalendarAPI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As always, treat user tokens as PII; never log them.&lt;/p&gt;

&lt;h2 id=&quot;6-long-running--async-tasks&quot;&gt;6. Long-Running &amp;amp; Async Tasks&lt;/h2&gt;

&lt;p&gt;Finally we arrive at the very complex scenario of long-running (perhaps hours or days long) tasks that may be initiated
by humans. I am &lt;em&gt;going out on a limb&lt;/em&gt; when describing the approaches that can be taken here, because they are not very
standardized, but I believe I have some ideas that can assist in making call chains like this more cryptographically
secure.&lt;/p&gt;

&lt;p&gt;Example: A user kicks off, or configures, an AI-driven workflow that runs for multiple hours and needs to utilize the
user’s identity to access various services. This workflow may even run in some type of scheduled way unattended from
there on out. The original JWT access_token representing the user expires after an hour, as is typical and proper with
most IdP’s. (Note: this is not unlike scenarios you can find in low-code tools like Power Platform.) We have some
options:&lt;/p&gt;

&lt;h3 id=&quot;61-refresh-tokens&quot;&gt;6.1. Refresh Tokens&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;The agent process (acting as an MCP client in this case) stores an (ideally encrypted at rest) &lt;strong&gt;refresh_token&lt;/strong&gt;
representing the user.&lt;/li&gt;
  &lt;li&gt;The agent process swaps the refresh_token for fresh JWT access_tokens with the IdP, scoped to desired target
resources, as needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is likely the most “proper” approach to this today, and has precedence in various services out there. But there is
a trade-off: storing a person’s refresh_token is essentially storing a session for them, and is inherently
high-privilege; thus these refresh_tokens should (and I would say, &lt;em&gt;must&lt;/em&gt;) be protected at rest. They can also be
revoked on user off-boarding, which is usually the responsibility of the IdP, but your implementation should gracefully
handle if/when a refresh_token is revoked.&lt;/p&gt;

&lt;h3 id=&quot;62-signed-job-token&quot;&gt;6.2. Signed Job Token&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;At task creation, the agent itself issues a JWT, perhaps by using the original user’s JWT or its signature to sign
the new JWT with a timestamp claim inside of it, with storage mechanisms for both user + newly issued JWT for later
verification (think along the lines of how we use code signing certs to sign source code—the code signing cert may be
long expired, but as long as it was valid &lt;em&gt;at signing time&lt;/em&gt;, it provides those integrity assurances):&lt;/p&gt;

    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{&quot;sub&quot;:&quot;alice&quot;, &quot;scope&quot;:&quot;export:1234&quot;, &quot;exp&quot;:now+3h, ...}&lt;/code&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Agent presents the original user’s now-expired JWT access_token &lt;em&gt;plus&lt;/em&gt; the newly minted JWT signed by the original
JWT access_token in HTTP headers to MCP endpoints dedicated to the job, and MCP knows to validate the original JWT
signature, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aud&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sub&lt;/code&gt;, but &lt;em&gt;not&lt;/em&gt; to pay attention to its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exp&lt;/code&gt; (expiration), but rather utilize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iat&lt;/code&gt; (issued
at) claim to understand if the child JWT with the timestamp was signed during a period when the original JWT was
valid.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this approach, there is no long-lived refresh_token required, and the scope is tightly bounded—however, it also
requires any endpoints that might receive such a JWT to implement very sophisticated logic of validating a child JWT
token based on its signature from another JWT, which many off-the-shelf JWT validation libraries will struggle to do
(which means you may be rolling up your sleeves to implement some of this).&lt;/p&gt;

&lt;h3 id=&quot;63-service-account-with-embedded-user-claims&quot;&gt;6.3. Service account with embedded user claims&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Worker runs with service auth only.&lt;/li&gt;
  &lt;li&gt;It stores userID + allowed resources in job metadata.&lt;/li&gt;
  &lt;li&gt;Every downstream query constrains rows by that userID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tolerance level for using basic strings to represent user context throughout short-lived and long-lived call chains
depends on many factors. Your ecosystem may or may not be tolerant to simple user id strings that could potentially be
spoofed if someone had an anchor point to call a backend service in your ecosystem. Further, there may be third party
API’s you wish to hit &lt;em&gt;as the user&lt;/em&gt; which you do not control, in which case you would inherently need some type of super
principal that has access to everything in that third party and extra logic to filter based on the user.&lt;/p&gt;

&lt;p&gt;Some type of cryptographic material representing the user (a refresh_token potentially that could be exchanged for
fresh JWT access_tokens for a given user when needed) will always be stronger than just representing the user in an
HTTP header like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x-user-id: &amp;lt;userId&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Pick the approach that aligns with your org’s security model and compliance burdens.&lt;/p&gt;

&lt;h2 id=&quot;7-state-of-todays-mcp-servers--a-gentle-rant&quot;&gt;7. State of Today’s MCP Servers – A Gentle Rant&lt;/h2&gt;

&lt;p&gt;Most open-source MCP implementations assume a trusted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; and a single user. Spin one up for a hackathon and
it’s magic; deploy it in prod and you’ll suddenly need:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Plug-and-play OAuth/OIDC support (ideally configurable with an IdP).&lt;/li&gt;
  &lt;li&gt;Multi-tenant storage abstractions out of the box for product scenarios.&lt;/li&gt;
  &lt;li&gt;Hooks for per-request security trimming.&lt;/li&gt;
  &lt;li&gt;Token exchange helpers (OBO, OAuth 2 Token Exchange draft).&lt;/li&gt;
  &lt;li&gt;Auditing and rate limits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Today you’ll find yourself bolting these on—or writing a custom gateway—because upstream doesn’t ship them. That’s not a
knock on the maintainers; it’s a sign the ecosystem grew up in a local-first culture. But the demand for &lt;em&gt;hosted&lt;/em&gt; MCP is
exploding, so the tooling, and more importantly the &lt;em&gt;patterns and practices&lt;/em&gt;, need to catch up.&lt;/p&gt;

&lt;h2 id=&quot;8-conclusion&quot;&gt;8. Conclusion&lt;/h2&gt;

&lt;p&gt;Context in MCP isn’t just the corpus you feed the model—it’s the environment your server inhabits. Keeping everything on
your laptop? Enjoy the blissful simplicity. The second you move to a team, a customer, or the public cloud, bring your
security A-game:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Authenticate every request.&lt;/li&gt;
  &lt;li&gt;Authorize &lt;em&gt;every&lt;/em&gt; slice of data.&lt;/li&gt;
  &lt;li&gt;Propagate identity with care.&lt;/li&gt;
  &lt;li&gt;Design for token lifecycles, not solely one-shot calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re building an MCP server, plan a two-mode strategy:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Dev mode&lt;/strong&gt;: Runs wide open on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; for fast iteration.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Prod mode&lt;/strong&gt;: Pluggable OAuth, per-tenant isolation, audit logs, and the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your future self—and your security team—will thank you.&lt;/p&gt;

&lt;p&gt;Ultimately, context is everything—not just in prompts, but in the infrastructure that serves them. Let’s make our MCP
tools as context-aware as the models they power.&lt;/p&gt;

&lt;h2 id=&quot;bonus-a2a-and-acp-protocols&quot;&gt;Bonus: A2A and ACP Protocols&lt;/h2&gt;

&lt;p&gt;Hot on the heels of MCP are the new
&lt;a href=&quot;https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/&quot;&gt;A2A&lt;/a&gt; and
&lt;a href=&quot;https://agentcommunicationprotocol.dev/introduction/welcome&quot;&gt;ACP&lt;/a&gt; protocols which can be used to complement MCP, and
while I won’t delve into the details of those in this blog post, I’ll just note that both require similar authentication
and authorization considerations to be enterprise-grade and production-worthy, and I may delve more into specifics
around those in a future blog post.&lt;/p&gt;

&lt;p&gt;I wrote this post mostly to address a lot of FUD (Fear, Uncertainty, and Doubt) around how MCP servers operate today and
their AuthN/AuthZ strategies (or lack thereof), and should the need arise I can take a similar stab at addressing A2A
and ACP.&lt;/p&gt;

&lt;p&gt;The world of AI is moving fast, and on the one hand it is great that we are getting open standards and specs so quickly,
but on the other hand it will take a little time for them to become fully “fleshed out” with enterprise-grade patterns
and practices.&lt;/p&gt;
</content:encoded>
        <pubDate>Fri, 30 May 2025 00:00:00 +0000</pubDate>
        <link>https://www.spletzer.com/2025/05/the-many-contexts-of-model-context-protocol/</link>
        <guid isPermaLink="true">https://www.spletzer.com/2025/05/the-many-contexts-of-model-context-protocol/</guid>
        
        <category>ai</category>
        
        <category>model-context-protocol</category>
        
        
      </item>
    
  </channel>
</rss>
