Your AI Agent Can See Your Passwords. Here’s How to Fix That.
You’re working with Claude Code, building against a WordPress REST API. You need it to authenticate, so you grab the App Password from your password manager, paste it into the prompt, and hit Enter.
Claude reads the credentials, constructs a curl command, fires it off. The response comes back. Everything works.
Then you look at the transcript.
Your App Password is right there — in the user message where you typed it and in the curl command Claude built. That credential is now part of the conversation history, where it can be logged, cached, or shared.
I’ve done this more times than I’d like to admit — pasted a key, got the result, moved on, and only later thought about what I’d left behind.
Export the conversation to hand off to a teammate? The password goes with it. (I was about to export a transcript for a colleague when I spotted an API key three messages up. That was a fun thirty seconds.) Same story if the session feeds future context or you share a transcript on a bug thread.
This risk shows up any time you hand an AI agent a secret — API keys, database URLs, any credential type. The agent uses the value in a tool call, and suddenly it’s baked into the transcript like a phone number scribbled on a napkin at a crowded restaurant. Anyone who picks up that napkin gets the number.

The model never needed the actual characters in the first place. A stable reference would do — a handle it could drop into commands while the real value stayed hidden.
.
.
.
What are Function Hooks?
Stay with me here — because the fix for this is more elegant than you’d expect.
Claude Code Function Hooks are a new capability, currently behind a feature flag and in a community feedback phase. If you want to follow the discussion, it’s GitHub proposal #91870, filed September 2026. Nothing here has officially shipped yet — everything in this post was tested against a working build, but the API could change before general availability.
Function Hooks are TypeScript middleware that wraps tool calls — they can intercept, modify, or replace any command before it executes, with shared state across the session. Think of them as a checkpoint between what you type and what the model sees, and another checkpoint between what the model asks to run and what actually executes.

.
.
.
The solution: a credential guard plugin
Two hooks working together can keep secrets out of the transcript while still letting commands execute with real credentials.
The first hook catches your input right when you hit Enter. It spots credentials, swaps each one for a safe placeholder like [WP-PASS-c481], and tucks the real value away for later. By the time the model sees your message, the secret is already gone.

The second hook watches for outgoing commands. When Claude builds a curl command using that placeholder, the hook swaps the real value back in right before execution. The command works. The transcript stays clean.


The same pattern works for any secret type — API keys, database connection strings, or any other credential.
.
.
.
Building it: the WordPress demo
Let me show you what this looks like end to end.
I wanted to test against a real API — something with actual authentication and a credential format worth detecting. WordPress App Passwords are a good candidate. They have a distinctive six-groups-of-four alphanumeric format, which makes them detectable by pattern matching. The scenario: install a WordPress plugin via the REST API using App Password auth. I used TasteWP for a throwaway test site, so there was no risk to a production environment.

Enable Function Hooks
Since the feature is behind a flag, the first step is opting in. Add this environment variable to your Claude Code settings:
{
"env": {
"CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1"
}
}
Describe the plugin
I used Claude Code’s /plugin-authoring skill and described what I wanted in five sentences of plain English. No code. No file structure. Just the behavior:
Create a plugin called "wp-credential-guard" at .claude/plugins/wp-credential-guard/.
On prompt.submit, find WordPress credentials — usernames and App Passwords — and
replace each with a stable placeholder like [WP-USER-xxxx] and [WP-PASS-xxxx]. Store
the real values in a module-level Map (never $.store — credentials must die with the
session). On tool.call for Bash, scan the command for those placeholders and swap in
the real values before execution so curl commands work but the transcript stays clean.
Five sentences.

What Claude built
And here’s the kicker — what came back was more thoughtful than the spec I’d written. Claude figured out how to recognize credentials in different formats, handled edge cases I hadn’t considered, and built in safeguards so the password swap wouldn’t break the command it was modifying.
After writing the code, it tested everything automatically — and caught a bug in its own work along the way (ferpetesake). Fixed it, re-tested, all passing.

Load the plugin
After the files are created, you restart Claude Code with the plugin directory:
claude --plugin-dir .claude/plugins/wp-credential-guard
The whole plugin is four files — a manifest, a module declaration, the TypeScript source with both hooks, and a compiler config.

A quick check of the installed plugins list confirms it loaded:

Test with real credentials
Now for the real test. I typed a prompt containing a REST API endpoint, a username, and a six-group App Password in plain text. No attempt to hide or encode the credentials — just a straightforward request to install a plugin:

The result
The password was fully redacted. Look at the transcript — the status line at the top reads “wp-credential-guard: masked 1 password (session-only).” Below that, the user message shows the App Password replaced with [WP-PASS-c481]. The real value, all six groups of it, is nowhere in the conversation.
When the placeholder showed up instead of my actual password, I scrolled back up to check twice. (Old habits. I wanted to believe it, but I also wanted to be sure.)
Claude then built a curl command using the placeholder and hit the WordPress REST API endpoint. From the transcript’s perspective, the password is just a bracketed token. Claude doesn’t know the difference, and it doesn’t need to. The hook swapped in the real value at execution time, and the API returned a successful response listing the site’s installed plugins.

What didn’t work
The username “smashed” was not redacted. App Passwords have a distinctive format that pattern matching can catch, but a username in prose looks like any other word. This is fixable with another prompt — tell Claude to add a username detection rule — and that iterative loop is the whole point of building with an AI agent.
.
.
.
Why this matters
If you’ve been using Claude Code for a while, you’ve probably tried the CLAUDE.md approach. I wrote about it in The Single File That Makes or Breaks Your Claude Code Workflow: put your rules in CLAUDE.md and trust the model to follow them. “Never log credentials.” “Always use environment variables for secrets.”
Here’s the thing: those rules work most of the time. But they compete with every other instruction in the context window — and the context window is a crowded place. In long conversations or complex tasks, a rule can get lost in the noise. A Claude Code Function Hook is deterministic. It runs on every tool call, every time, regardless of how long the conversation has been or how much context the model is juggling.

There’s a second benefit worth calling out: each rule you move into a hook is one fewer instruction consuming context tokens. I talked about context management in Claude Code Sandbox Explained: Stop Pressing Enter 50 Times a Day, where the sandbox saves you permission fatigue. Hooks save you context space. Both free up room for the instructions that actually need the model’s attention.
If you’ve seen how secrets management tools handle credentials in CI/CD pipelines, this pattern will feel familiar. Tools like Infisical run a local proxy that injects secrets at the edge — the application references placeholders, the proxy swaps in real values at request time, and the secrets never appear in logs or config files. Claude Code Function Hooks follow the same principle: the secret gets injected at execution time, invisible to the model orchestrating the work.
👉 Credentials are the most obvious use case, but the pattern applies to anything you want to keep out of the transcript while still using in tool calls.
This is where AI agent tooling is heading. The plugin system means you don’t have to be a security engineer to build a credential guard — five sentences of plain English got me a working one. Function Hooks are still in early access, which means now is a good time to start experimenting before the community settles on conventions.
Try it yourself
- Enable Function Hooks in your Claude Code settings with the environment variable shown earlier in this post
- Use
/plugin-authoringto describe a credential guard in your own words - Test it against a real API call with a throwaway credential
- Check the transcript. The real value should never appear.
Leave a Comment