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:
- it's sent to Anthropic's API as part of your prompt,
- it's written to your local
*.jsonltranscript, - 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:
-
detects it (regex for ~20 providers, explicit
/keymarkers, optional gitleaks), -
saves the value to
~/.claude/secrets/<name>.txt(chmod 600), - blocks the original prompt so the raw value never reaches the model,
-
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}))
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:
-
macOS —
osascript(needs Accessibility permission) -
Linux X11 —
xdotool -
Linux Wayland —
wtype(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
…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
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)
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.
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