The Silent Leak: How AI Agent Surfaces Credentials in Plaintext
01: THE INCIDENT
The five words that started this
I asked an AI agent to fix a git submodule that wasn’t populating in a repos. Nothing dramatic, a five-minute ask. It found the submodule had never been initialized, worked through a slow and repeatedly stalling clone, and at one point ran a standard diagnostic to rule out authentication as the cause. The kind of thing any of us would type without thinking twice:
$ echo "url=https://git.internal.example.com" | git credential fill protocol=https host=git.internal.example.com username=alice password=##########
I hadn’t asked for a credential. I hadn’t typed one. It was just sitting there in my chat transcript, in the clear. So I asked the only question that made sense: how you got the creds?
Turns out the answer wasn’t dramatic at all. git credential fill is documented git plumbing the same call git makes internally whenever an HTTPS operation needs to authenticate. No jailbreak. No injection. No bug in sight. The agent ran it exactly the way it’s meant to be run, and that’s what makes it worth writing about: nothing malicious happened, nothing unusual was typed, just an agent with shell access doing ordinary, well-meant work, in a transcript that outlives the moment it was created.
What turned an uncomfortable moment into an actual write-up was the obvious follow-up: if this door was this ordinary, how many others look just like it? That question is what the rest of this piece is answering. Section 03 covers why having an agent as the operator changes the risk at all. Section 04 is the catalog that came out of going door to door, mapped against MITRE ATT&CK. Section 05 is the checklist for closing them.
02: THE MECHANISM
At rest vs. in transit
Credential managers genuinely encrypt secrets at rest. Windows Credential Manager, macOS Keychain, pass, git-credential-libsecret on Linux (backed by the encrypted GNOME Keyring or KDE Wallet) all do this honestly. That part isn’t where things go wrong.
Worth a quick footnote: git-credential-cache doesn’t belong on that list. It never encrypts anything, it just holds the plaintext in a background daemon’s memory for a configurable window (15 minutes by default), protected only by Unix-socket file permissions. Different mechanism, same ending: something, somewhere, ends up holding the plaintext.
The real problem is in the interface. Every credential-helper protocol has a “fill” or “get” operation, and its whole job is to decrypt and hand back plaintext, because whatever’s consuming the credential needs the real value to authenticate. That handoff is supposed to happen invisibly, between two trusted local processes. It was never meant to touch a terminal a human can read, let alone a language model’s context window.
An agent with shell access breaks that invisibility, and not through some flaw in how it’s built, just by existing the way it does. It doesn’t consume a credential internally the way git does. It runs the command as a subprocess and reads whatever comes back on stdout as plain data to reason over. So whatever the protocol hands back, the agent now holds as text, no different, as far as it’s concerned, from any other line of output.
03: WHY IT'S DIFFERENT NOW
What changes when the operator is an agent
Run git credential fill in a one-off shell script and you’ve got the same theoretical risk. What actually makes agentic tooling different is everything that happens to the output after the command returns.
- [PERSISTENCE]
Shell output in a terminal disappears when the window closes. Agent transcripts are built to persist, as conversation history, as memory the agent writes to disk, as shared links, as logs kept for debugging.
- [NO WATCHER]
Background and asynchronous agents run unattended for minutes or hours. A secret can surface and be written to a log well before any human reviews the session.
- [NO JUDGMENT]
The agent has no innate sense that a line of output is sensitive. It treats
password=xyzexactly like a directory listing, just tokens to reason about. Redaction has to come from outside the model, not from it. - [RETRY]
An agent chasing a flaky connection may re-run the same "helpful" diagnostic several times. Each run is a fresh exposure event, not a repeated view of the same one.
- [PROPAGATION]
A transcript pasted into a bug report, exported for a teammate, or fed into a summarization pipeline carries the secret with it, well past the context where it first appeared.
04: THE CATALOG
A taxonomy of surfacing vectors
git credential fill is one door. It’s nowhere near the only one. What follows splits them into two groups: vectors with a confirmed ATT&CK sub-technique, and vectors that leak just as reliably but that the framework simply hasn’t named yet. As far as I could find while researching this, some of these vectors have a clear, confirmed technique to point to, and others don’t, though they still broadly fall under one.
[TTP] Confirmed ATT&CK sub-technique
| Category | Example commands | Why it leaks | ATT&CK |
|---|---|---|---|
| Credential-helper introspection | git credential fillsecurity find-generic-password -wGet-StoredCredential (CredentialManager PS module) | The command's entire purpose is decrypt-and-print. | T1555, T1555.001, T1555.004 |
| Password manager/browser stores | Chrome "Login Data" sqlite + OS decrypt call (DPAPI/Keychain) | The stored blob is encrypted at rest, but the decryption key is scoped to the logged-in OS user, any process running as that user can decrypt it the same way the browser does. | T1555.003 |
| Unencrypted private key files | cat ~/.ssh/id_rsa (no passphrase) | A passphrase-less key is usable the instant it's read, no decrypt step required. | T1552.004 |
| Environment/config dumps | envprintenvcat ~/.netrccat ~/.git-credentialscat ~/.aws/credentials | Secrets are stored as plaintext files or variables by design. | T1552.001 |
| Package manager auth files | cat .npmrccat .pypirccat NuGet.Config | Registry tokens commonly live in plaintext config so the package manager can authenticate non-interactively. | T1552.001 |
| Kubernetes secret stores | kubectl get secret -o yaml | Base64 is encoding, not encryption,trivially reversible. | T1552.007 |
| Process & history introspection | ps auxshell history files docker inspect | CLI arguments and history aren't treated as secret-safe by most tools. | T1552.003, T1057, T1552.007 |
| Cloud metadata/token endpoints | curl 169.254.169.254/... (token hop for IMDSv2)gcloud auth print-access-token | Instance-role credentials are fetched in cleartext by design. | T1552.005 |
| Browser/session capture | screenshots HAR file exports DOM dumps during automation | Auth headers, cookies, and autofilled passwords are visible in the captured artifact even when never printed to a terminal. | T1113, T1539 |
All nine sit under the Credential Access tactic (TA0006) except T1057 (Process Discovery, TA0007, Discovery) and T1113 (Screen Capture, TA0009, Collection).
[POSSIBLE TTP] No dedicated sub-technique, closest parent shown
| Category | Example commands | Why it leaks | Closest ATT&CK |
|---|---|---|---|
| Infrastructure-as-code state | terraform showcat terraform.tfstate | Provisioned-resource secrets are frequently written into state files unencrypted. | T1552.001 |
| Verbose/debug flags | curl -vGIT_CURL_VERBOSE=1 git fetch | Debug tracing exists specifically to print headers, including Authorization, that are normally hidden. | T1552 |
| Cross-process environment reads | cat /proc/<pid>/environ (same user or root) | Any process's environment, including secrets passed to it, is readable by anything sharing its user permissions. | T1003.007 |
| Version-control archaeology | git log -p --allgit show <old-sha>git stash list | A secret "removed" from the current branch tip often still lives in git's object database. | T1552.001 |
| CI/CD pipeline logs | echo $SECRET_VARpartial/transformed variable interpolation | Log masking is pattern-based; a re-encoded or substring-echoed secret slips past the mask. | T1552.001 |
| Error messages & stack traces | Unhandled exceptions with connection strings | Debug-friendly errors often embed the secret used to connect. | T1552.001 |
| URLs with embedded auth | Presigned URLs, API keys in query strings | The secret is the URL, no special command needed at all. | T1552.001 |
Every technique here still falls under T1552 (Unsecured Credentials, TA0006) at the parent level. Enterprise ATT&CK is built around hosts and networks, and it simply hasn’t caught up to source-code-native vectors like git history mining, CI/CD log masking, or debug-flag output, none of those have their own sub-technique yet. Read the ID here as “closest neighbor,” not “exact match.”
MITRE’s AI-specific extension, ATLAS, mirrors the two anchor techniques above as AML.T0055 (Unsecured Credentials) and AML.T0090 (OS Credential Dumping). Its newer agent and tool-invocation-specific credential-harvesting additions don’t have stable published IDs yet, so I’ve left them out rather than guess.
A deliberate omission: SSH agent forwarding isn’t in either table. It’s a real risk, just a different kind, a forwarded agent lets a remote host use your key to authenticate without the key’s bytes ever leaving your machine. That’s someone misusing a credential, not a credential leaking in plaintext, and it earns its own write-up rather than a spot in this list.
05: THE RESPONSE
What actually needs monitoring
Telling an agent to “be careful” is a policy. It’s not a control. The mitigations that actually hold in practice sit between the shell and the model’s context, not inside the model’s judgment:
- [GUARDRAIL]
Deny-list, or require explicit human approval for, known secret-revealing patterns:
credential fill/approve,*/.netrc,*/.git-credentials, unfilteredenv. Cheapest, highest-leverage control available. - [SCANNER]
Run every tool's stdout/stderr through a secret scanner, entropy and regex rules, the same class used by gitleaks or trufflehog, before it re-enters the model's context. Matches get replaced with a token like
[REDACTED:aws_secret_key], never shown. - [SPLIT LOG]
Keep the full raw output in an access-controlled audit log for humans. The model, and anything downstream of its context, including memory and shared transcripts, sees only the redacted version.
- [SAFER CHECK]
Prefer verification that confirms without revealing:
git ls-remoteoraws sts get-caller-identityconfirm a credential is valid without printing it. Reach for these before anything with "fill," "show," or "print" semantics. - [SHORT-LIVED]
A surfaced long-lived PAT is compromised for as long as it's valid, often months. OIDC/STS-issued tokens shrink that window to minutes-to-an-hour, capping the damage of any single exposure.
- [SANDBOX]
Ideally the agent's shell never has direct query access to the real credential store. Secrets get injected only at the point of use, by a broker process the agent can't introspect.
- [ROTATE]
Treat any exposure as compromised the moment it appears in a transcript, encrypted-at-rest storage of the transcript doesn't change that calculus. Rotate first; investigate after.
- [CANARY]
Seed a small number of fake, monitored credentials in the store. A hit on one is a strong signal that something, human or agent, is enumerating secrets it shouldn't.
CLOSING
The honest takeaway
Every credential-helper protocol has a plaintext handoff built into it, with or without AI anywhere near the loop. That’s not new, and it’s not really the point. The point is how ordinary the moment was: an agent doing routine, well-meant troubleshooting reached for that handoff without any signal, to itself or to anyone watching, that this step was different in kind from listing a directory.
The fix isn’t a smarter agent. It’s a boundary the agent can’t reason its way around: control over which commands are allowed to run, and a scanner standing between what the shell returns and what the model is permitted to remember.
Until that boundary is standard in agentic tooling, assume the conservative case: any credential that touches an agent’s shell is one command away from ending up in a transcript, a log, or a shared link. Provision, scope, and rotate it like that’s already true.
