Skip to content
ISSUE #67 Sep 11, 2026 8 MIN READ

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 leak path: a pasted password lands in the transcript, then travels wherever the transcript goes — exported to a teammate, cached in context, or shared for debugging

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.

Middleware pipeline showing how a prompt.submit hook intercepts user input before Claude sees it, and a tool.call hook intercepts commands before they execute

.

.

.

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.

On Input: the prompt.submit hook scans for credentials, replaces them with a placeholder like WP-PASS-c481, and stores the real value in an in-memory vault

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.

On Execution: the tool.call hook retrieves the real value from the vault and swaps it into the command before Bash executes
Side-by-side comparison: without a hook, the App Password appears in both the transcript and the curl command; with a hook, the transcript shows only a placeholder and the real value is restored at execution time

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.

Demo setup: Claude Code with the wp-credential-guard plugin connecting to the WordPress REST API on a TasteWP test site, with credentials redacted in transit

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.

The /plugin-authoring prompt in Claude Code, a five-sentence plain English description of what the credential guard plugin should do

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.

Claude's build summary showing the plugin architecture: in-memory vault design, seven ordered detection rules, shell-quoting-aware restoration, and all twenty verification tests 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.

File tree showing the four plugin files: plugin.json manifest, hooks.json module declaration, hooks.ts with the two hooks, and tsconfig.json

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

Claude Code's installed plugins list with wp-credential-guard showing as enabled under the User section

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 user prompt containing a real WordPress App Password in plain text, asking Claude to install a plugin via the REST API

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.

The transcript showing the password replaced with the placeholder WP-PASS-c481, the status line confirming one password masked, the curl command using the placeholder, and the API returning a successful response

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.

CLAUDE.md rule versus Function Hook: a rule competes for attention among many instructions in the context window, while a hook runs deterministically on every tool call

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

  1. Enable Function Hooks in your Claude Code settings with the environment variable shown earlier in this post
  2. Use /plugin-authoring to describe a credential guard in your own words
  3. Test it against a real API call with a throwaway credential
  4. Check the transcript. The real value should never appear.

Nathan Onn

Freelance web developer. Since 2012 he’s built WordPress plugins, internal tools, and AI-powered apps. He writes The Art of Vibe Coding, a practical newsletter that helps indie builders ship faster with AI—calmly.

Join the Conversation

Leave a Comment

Your email address will not be published. Required fields are marked with an asterisk (*).

Enjoyed this post? Get similar insights weekly.