DEV Community

Cover image for Stop leaking API keys into Claude Code
Alberto Migliorato
Alberto Migliorato

Posted on

Stop leaking API keys into Claude Code

Every time I pasted an API key into Claude Code, I'd get the little warning: "this key may have leaked — rotate it."

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.

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.

It's called Keyward — a small, open-source Claude Code plugin. Here's the idea, plus the one genuinely tricky part of building it.

The problem, precisely

When you paste a secret into the chat:

  1. it's sent to Anthropic's API as part of your prompt,
  2. it's written to your local *.jsonl transcript,
  3. Claude (correctly) tells you it leaked.

Three places, one paste. The damage is done before you finish reading the warning.

What Keyward does

A UserPromptSubmit hook scans every message you submit. When it spots a key it:

  1. detects it (regex for ~20 providers, explicit /key markers, optional gitleaks),
  2. saves the value to ~/.claude/secrets/<name>.txt (chmod 600),
  3. blocks the original prompt so the raw value never reaches the model,
  4. re-submits a sanitized version of your message — the key replaced by <<secret:NAME stored at ~/.claude/secrets/NAME.txt>>.

You press Enter once; the model only ever sees the reference.

The tricky part: you can't modify a prompt, only block it

This is the bit worth sharing, because it surprised me.

Claude Code's UserPromptSubmit hook runs before your message reaches the model — the perfect place to catch a key. But by design, that hook cannot rewrite your prompt. It can only:

  • add context alongside it, or
  • block it entirely.

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").

So "detect the key and quietly swap it" isn't directly possible. Keyward's workaround:

# intercept.py (simplified)
detection = detect(prompt)
if detection["secrets"]:
    for s in detection["secrets"]:
        save_secret(s["name"], s["value"])        # atomic write, chmod 600
    sanitized = sanitize_prompt(prompt, detection["secrets"])
    write_tempfile(sanitized)                      # + put on clipboard
    spawn_paste(sanitized)                         # OS-level keystrokes
    print(json.dumps({"decision": "block",
                      "suppressOriginalPrompt": True}))
Enter fullscreen mode Exit fullscreen mode

The hook blocks the leaking prompt and spawns a tiny detached process that, via OS-level automation, 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.

Cross-platform paste

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

  • macOSosascript (needs Accessibility permission)
  • Linux X11xdotool
  • Linux Waylandwtype (compositor-dependent: Sway/Hyprland yes, GNOME not by default)
  • Windows — PowerShell SendKeys

No display server (SSH, Docker)? Set KEYWARD_DISABLE_PASTE=1 — it still saves and sanitizes, you paste manually.

Using a saved key without re-leaking it

A bundled skill teaches Claude to consume the saved secret without printing it:

export GITHUB_TOKEN=$(cat ~/.claude/secrets/github_pat_classic.txt) && gh api /user
Enter fullscreen mode Exit fullscreen mode

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

Honestly: it's defense-in-depth, not magic

Secrets are stored as chmod 600 plaintext — the same trust model as ~/.aws/credentials or a .env file, not encrypted at rest. Keyward is a safety net for the "I need to use this key once, in chat, now" 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.

Try it

In a Claude Code session:

/plugin marketplace add albemiglio/keyward
/plugin install keyward@keyward
Enter fullscreen mode Exit fullscreen mode

Free and open source (MIT) — github.com/albemiglio/keyward. ~35 tests, CI across macOS/Linux/Windows, no network calls, no telemetry.

If you've ever pasted a key into an AI tool and hoped for the best — this is the net. 🔑

Top comments (2)

Collapse
 
theoephraim profile image
Info Comment hidden by post author - thread only accessible via permalink
Theo Ephraim

Use varlock.dev - it helps remove all keys from plaintext. Either via encryption or by fetching declaratively. Also does log redaction and leak prevention. Lots of other great features. Free and open source.

Collapse
 
albemiglio profile image
Alberto Migliorato

Thanks — varlock looks genuinely excellent. The .env.schema + declarative op() fetching + log redaction combo is a really clean take on the config layer.

I think we sit at different boundaries, though. varlock secures how your app and dev environment hold secrets — at rest, at runtime, in CI. Keyward catches a narrower, more human moment: the instant you paste a raw key into the Claude Code chat box to get something done now. That paste is already in the model's context and your session transcript before it's ever in a config file — that's the gap Keyward plugs.

So honestly they compose: varlock for the secrets my code depends on, Keyward as the net for the "ugh, I just pasted a live token into chat" slip. Going to give it a proper look — the leak-scanning especially. 🙏

Some comments have been hidden by the post's author - find out more