Post

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:

sh
$ echo "url=https://git.internal.example.com" | git credential fill
protocol=https
host=git.internal.example.com
username=alice
password=##########
password redacted for this write-up, the real output was a valid, working credential

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=xyz exactly 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

CategoryExample commandsWhy it leaksATT&CK
Credential-helper introspectiongit credential fill
security find-generic-password -w
Get-StoredCredential (CredentialManager PS module)
The command's entire purpose is decrypt-and-print.T1555,
T1555.001,
T1555.004
Password manager/browser storesChrome "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 filescat ~/.ssh/id_rsa (no passphrase)A passphrase-less key is usable the instant it's read, no decrypt step required.T1552.004
Environment/config dumpsenv
printenv
cat ~/.netrc
cat ~/.git-credentials
cat ~/.aws/credentials
Secrets are stored as plaintext files or variables by design.T1552.001
Package manager auth filescat .npmrc
cat .pypirc
cat NuGet.Config
Registry tokens commonly live in plaintext config so the package manager can authenticate non-interactively.T1552.001
Kubernetes secret storeskubectl get secret -o yamlBase64 is encoding, not encryption,trivially reversible.T1552.007
Process & history introspectionps aux
shell 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 endpointscurl 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 capturescreenshots
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

CategoryExample commandsWhy it leaksClosest ATT&CK
Infrastructure-as-code stateterraform show
cat terraform.tfstate
Provisioned-resource secrets are frequently written into state files unencrypted.T1552.001
Verbose/debug flagscurl -v
GIT_CURL_VERBOSE=1 git fetch
Debug tracing exists specifically to print headers, including Authorization, that are normally hidden.T1552
Cross-process environment readscat /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 archaeologygit log -p --all
git 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 logsecho $SECRET_VAR
partial/transformed variable interpolation
Log masking is pattern-based; a re-encoded or substring-echoed secret slips past the mask.T1552.001
Error messages & stack tracesUnhandled exceptions with connection stringsDebug-friendly errors often embed the secret used to connect.T1552.001
URLs with embedded authPresigned URLs, API keys in query stringsThe 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, unfiltered env. 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-remote or aws sts get-caller-identity confirm 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.

This post is licensed under CC BY 4.0 by the author.