<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alberto Migliorato</title>
    <description>The latest articles on DEV Community by Alberto Migliorato (@albemiglio).</description>
    <link>https://dev.to/albemiglio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3981452%2F34195d90-a480-4e2e-838b-4c26eb86257f.png</url>
      <title>DEV Community: Alberto Migliorato</title>
      <link>https://dev.to/albemiglio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/albemiglio"/>
    <language>en</language>
    <item>
      <title>Stop leaking API keys into Claude Code</title>
      <dc:creator>Alberto Migliorato</dc:creator>
      <pubDate>Fri, 12 Jun 2026 16:26:04 +0000</pubDate>
      <link>https://dev.to/albemiglio/stop-leaking-api-keys-into-claude-code-5en1</link>
      <guid>https://dev.to/albemiglio/stop-leaking-api-keys-into-claude-code-5en1</guid>
      <description>&lt;p&gt;Every time I pasted an API key into Claude Code, I'd get the little warning: &lt;em&gt;"this key may have leaked — rotate it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And… yeah. I know. By then the key is already in the request, and already sitting in my session transcript forever. Rotating it is the chore, not the fix.&lt;/p&gt;

&lt;p&gt;I didn't want to rotate a key every time I referenced one, and "just be careful" stops working the moment you're moving fast — which, if you're vibecoding, is most of the time. So I made the careful part automatic.&lt;/p&gt;

&lt;p&gt;It's called &lt;strong&gt;Keyward&lt;/strong&gt; — a small, open-source Claude Code plugin. Here's the idea, plus the one genuinely tricky part of building it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, precisely
&lt;/h2&gt;

&lt;p&gt;When you paste a secret into the chat:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it's sent to Anthropic's API as part of your prompt,&lt;/li&gt;
&lt;li&gt;it's written to your local &lt;code&gt;*.jsonl&lt;/code&gt; transcript,&lt;/li&gt;
&lt;li&gt;Claude (correctly) tells you it leaked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three places, one paste. The damage is done before you finish reading the warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Keyward does
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook scans every message you submit. When it spots a key it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;detects&lt;/strong&gt; it (regex for ~20 providers, explicit &lt;code&gt;/key&lt;/code&gt; markers, optional gitleaks),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;saves&lt;/strong&gt; the value to &lt;code&gt;~/.claude/secrets/&amp;lt;name&amp;gt;.txt&lt;/code&gt; (&lt;code&gt;chmod 600&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;blocks&lt;/strong&gt; the original prompt so the raw value never reaches the model,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;re-submits&lt;/strong&gt; a sanitized version of your message — the key replaced by &lt;code&gt;&amp;lt;&amp;lt;secret:NAME stored at ~/.claude/secrets/NAME.txt&amp;gt;&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You press Enter once; the model only ever sees the reference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tricky part: you can't modify a prompt, only block it
&lt;/h2&gt;

&lt;p&gt;This is the bit worth sharing, because it surprised me.&lt;/p&gt;

&lt;p&gt;Claude Code's &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook runs &lt;em&gt;before&lt;/em&gt; your message reaches the model — the perfect place to catch a key. But by design, that hook &lt;strong&gt;cannot rewrite your prompt&lt;/strong&gt;. It can only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add context &lt;em&gt;alongside&lt;/em&gt; it, or&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;block&lt;/strong&gt; it entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a deliberate safety choice: a hook that could silently rewrite your prompt would be a nasty attack surface (imagine a plugin quietly turning "delete file X" into "delete file Y").&lt;/p&gt;

&lt;p&gt;So "detect the key and quietly swap it" isn't directly possible. Keyward's workaround:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# intercept.py (simplified)
&lt;/span&gt;&lt;span class="n"&gt;detection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secrets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secrets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;save_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;        &lt;span class="c1"&gt;# atomic write, chmod 600
&lt;/span&gt;    &lt;span class="n"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sanitize_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secrets&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;write_tempfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sanitized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# + put on clipboard
&lt;/span&gt;    &lt;span class="nf"&gt;spawn_paste&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sanitized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c1"&gt;# OS-level keystrokes
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;block&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;suppressOriginalPrompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hook blocks the leaking prompt and spawns a tiny detached process that, via &lt;strong&gt;OS-level automation&lt;/strong&gt;, pastes the sanitized text and hits Enter for you. From your side: a blocked message flashes, then a clean one takes its place. One keypress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-platform paste
&lt;/h2&gt;

&lt;p&gt;The paste backend is per-OS, and each one saves your clipboard, types, verifies focus hasn't changed, and restores the clipboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; — &lt;code&gt;osascript&lt;/code&gt; (needs Accessibility permission)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux X11&lt;/strong&gt; — &lt;code&gt;xdotool&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux Wayland&lt;/strong&gt; — &lt;code&gt;wtype&lt;/code&gt; (compositor-dependent: Sway/Hyprland yes, GNOME not by default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; — PowerShell &lt;code&gt;SendKeys&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No display server (SSH, Docker)? Set &lt;code&gt;KEYWARD_DISABLE_PASTE=1&lt;/code&gt; — it still saves and sanitizes, you paste manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using a saved key without re-leaking it
&lt;/h2&gt;

&lt;p&gt;A bundled skill teaches Claude to consume the saved secret &lt;em&gt;without printing it&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.claude/secrets/github_pat_classic.txt&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; gh api /user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…never a bare &lt;code&gt;cat&lt;/code&gt; (which would dump the value straight back into the context). The value flows disk → process env → tool, and never through stdout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honestly: it's defense-in-depth, not magic
&lt;/h2&gt;

&lt;p&gt;Secrets are stored as &lt;code&gt;chmod 600&lt;/code&gt; plaintext — the same trust model as &lt;code&gt;~/.aws/credentials&lt;/code&gt; or a &lt;code&gt;.env&lt;/code&gt; file, not encrypted at rest. Keyward is a safety net for the &lt;strong&gt;"I need to use this key once, in chat, now"&lt;/strong&gt; workflow, not a replacement for a real secret manager. The README and wiki document exactly what it does and doesn't protect, which feels like the right thing for a security tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;In a Claude Code session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add albemiglio/keyward
/plugin install keyward@keyward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free and open source (MIT) — &lt;strong&gt;&lt;a href="https://github.com/albemiglio/keyward" rel="noopener noreferrer"&gt;github.com/albemiglio/keyward&lt;/a&gt;&lt;/strong&gt;. ~35 tests, CI across macOS/Linux/Windows, no network calls, no telemetry.&lt;/p&gt;

&lt;p&gt;If you've ever pasted a key into an AI tool and hoped for the best — this is the net. 🔑&lt;/p&gt;

</description>
      <category>claude</category>
      <category>opensource</category>
      <category>security</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
