Unencrypted secrets on developer workstations and long-lived credentials in continuous integration environments have long been considered bad practice. But they have often been tolerated because the cost of avoiding them was perceived as higher than the cost of leaving them - for most projects, most of the time. That calculation has changed. The rise of AI agents has opened new paths for local secrets to leak, and every step of the software development lifecycle is being actively targeted by a sustained campaign of supply chain attacks. Rigorous secrets management is now a foundational part of every project.
What changed
1. Supply chain meltdown
Credential harvesting attacks targeting developer workstations and CI environments have reached a critical mass. The pattern was set in late 2025 with the Shai-Hulud self-replicating worm that affected hundreds of npm packages - with millions of combined weekly downloads - and systematically harvested credentials from machines they were installed upon.
Since then, the picture has only worsened.
- In March 2026, two versions of the Axios HTTP client, a JavaScript library with close to 100
million weekly downloads, were injected with a malicious
payload
containing a remote-access trojan activated via an npm post-install hook. According
to security researcher John Hammond the trojan
“performed immediate system reconnaissance: enumerating user directories, filesystem drive roots,
and running processes, and transmitted this data to the [control server]”. All that was needed was to
npm installat the wrong moment. Any passwords, tokens or private keys stored on the machine were quickly exfiltrated. - Also in March 2026, two versions of the
litellmPython package were injected with malware after a maintainer’s PyPI account was compromised.litellmis a router for AI APIs and claimed over three million daily downloads at the time of the incident. According to the project blog, the payload contained a credential harvester which scanned for environment variables, SSH keys and cloud provider credentials and transmitted them to a control server. This exploit was followed in April by the compromise of the PyTorch Lightning PyPI package, another library used in AI workflows, which was also injected with credential stealing malware. - In April 2026, the
@bitwarden/clipackage on npm was compromised. Bitwarden is an established password manager used by over ten million people and 50,000 businesses. The affected package, while only used by a minority of those users, is popular with developers and commonly used in CI workflows, the direct target of the attackers. The compromised version again contained credential harvesting malware that targeted SSH keys, npm credentials and cloud provider tokens as well as - notably - Claude configuration files likely to contain MCP server credentials. It took ninety minutes for the malicious release to be detected and removed from npm.
CI environments have been no less of a target.
- In March 2026, a compromised version of the Trivy security scanner was published, a breach which according
to Palo Alto Networks exposed CI/CD
secrets, planted persistent backdoors on developer machines, and spread a self-propagating worm across dozens of npm
packages. It harvested SSH keys, cloud credentials (AWS, GCP, Azure), Kubernetes tokens, Docker registry credentials,
database passwords, and TLS private keys and, once again, exfiltrated everything to a control server. This compromise
was the likely entrypoint for threat actors to compromise the
litellmpackage, as described above. - In April 2026, malicious images were pushed to the
checkmarx/kicsDocker repository. KICS is an open source software security tool with over 5 million total downloads. The malware captured scan output - potentially including secrets, credentials, cloud resource names, and internal topology - and sent them to an attacker-controlled endpoint.
The above examples cover little more than a month of 2026, and are by no means an exhaustive list.
We can speculate about why such attacks have become so widespread. Perhaps the economics have shifted so that it’s now more profitable to go after developers’ credentials than it is to install ransomware or cryptominers. Some campaigns have been attributed to state-aligned actors, who may have both financial and strategic reasons to target developer infrastructure. AI may also have played a role, either in helping malicious actors move faster or by lowering the barrier to building software and expanding the pool of developers yet to be widely exposed to foundational security practices. Regardless, the trend is unmistakable. Unencrypted secrets are at greater risk of exfiltration than ever.
2. Agents
In April 2026, the software company PocketOS lost three months of customer data when its production database, and all of its backups, was deleted by an AI agent. The agent, Cursor running Claude Opus 4.6, used a production API token stored unencrypted on the user’s machine to perform the deletion of its own accord while ostensibly attempting to resolve a configuration issue.
As you’d expect, this was an incident that featured a cascading series of missing or inadequate guardrails. But a critical aspect of the story is that the agent acted without explicit direction. It was never instructed to read the API token, let alone to use it. The model had been trained and prompted to avoid destructive actions, and operated inside an agent harness that layered on further safeguards. But these are only soft constraints. Ultimately, LLMs are probabilistic token-generating machines and prompting can only reduce the likelihood of them generating a fateful sequence. And the harness cannot be expected to catch every possible failure mode. Your production database only needs to be unlucky once. Ensuring its safety requires hard constraints: first and foremost, don’t give your agents access to credentials you are not happy to be used.
The above story is a worst case scenario (albeit not the only example) but agents increase credential exposure in a range of other ways. The Model Context Protocol (MCP) ecosystem has grown substantially over the last year, with thousands of servers offering to connect your agents to a wide selection of external APIs and services. The MCP protocol itself has known structural security gaps beyond the scope of this article, but relevant here is the general trend of developers wiring often broadly-scoped production credentials into their local environments in ways that wouldn’t have happened three years ago. These tokens typically sit unencrypted in harness config files, now yet another prime target for credential harvesters. The Bitwarden compromise described above appears to have targeted exactly this class of credentials.
A further vector for credential exfiltration is prompt injection. This occurs when agents have their instructions or context poisoned by untrusted remote sources - think web pages, Git repositories or npm packages. Consequences vary, but turning the agent itself into a local credential harvester is an obvious goal. In April 2026, Google found that prompt injection is happening in the wild, including with the explicit aim of data exfiltration and destruction. In the same month, Forcepoint reported on a series of real-world prompt injection attacks. This class of attack is still evolving, and other mitigations do exist - agent sandboxing, for example. But defence in depth requires consideration of what happens if sandboxing is missing or broken. The answer should be that credentials remain secure.
Once again, this is not an exhaustive list. The agent ecosystem is complex and opens up many possible vulnerabilities
but the broad implications are already clear. Credentials can no longer sit passively in .env files. Proper, active
management is needed. But what does that look like?
What to do instead
Effective secrets management is a posture, not a matter of specific tools or processes (although some can help, as we’ll see). Credential exposure must be systematically minimized at every step of the software development lifecycle, from developer workstations through CI to production.
At every stage four principles apply. All draw directly from long-established security best practices. In descending order of effectiveness:
- Don’t store the credential at all. A credential that does not exist cannot be stolen.
- Don’t store the credential in plain text. This mitigates the risk of exfiltration of at-rest credentials, although careful management of encryption keys is still required.
- Don’t store credentials that stay valid longer than necessary. If the first two principles fail, a short-lived credential has a smaller window for exploitation and little or no financial value to an attacker.
- Don’t store credentials that carry more permissions than necessary. If the first two principles fail, a tightly-scoped credential has a smaller blast radius and can do less damage.
In the past, applying these principles was frequently deferred or at least minimized until real users or revenue arrived. That is no longer an option. Any software project, no matter how early-stage, should be built around these principles from day one. Retrofitting an existing project takes time and the exact process differs for every environment, but each stage offers room for concrete improvements. Let’s walk through a few examples, starting on your own laptop.
1. Avoid long-lived credentials on your development machine
Amongst the most obvious wins is simply removing long-lived credentials from your development machine. As the first principle states, a credential that does not exist is a credential that cannot be harvested.
The exact location of such credentials depends on your choices of providers and tooling. Providing a comprehensive list is impossible, and tracking down every stray token on a long-established laptop can be challenging. If you can afford the time, an informative process is to re-install your development workstation from scratch, paying close attention to where every required credential is written.
If that’s not possible, there are some good places to start. Cloud provider credentials are as high value as they come
to any attacker, and ~/.aws/credentials is one of the first locations any credential-harvesting malware will check.
Don’t store long-lived IAM user access tokens there. Instead, configure AWS Identity
Center (especially convenient if you’re
managing multiple AWS accounts) and authenticate with aws login which will issue short-lived STS tokens scoped to your
assigned permission set.
A further prime target for malware is ~/.npmrc. Even the modern web-based npm login flow still
stores long-lived unencrypted access tokens here. It is most effective to store no credentials at
all so just avoid logging into npm locally at all if possible. Package publishing should happen from a
dedicated CI flow which can support short-lived OIDC tokens, discussed in more detail below.
Agent harness configurations are a third high-value target. Files like ~/.claude.json typically store credentials for
every configured MCP server in plain text, and neither the protocol nor most servers offer a more secure alternative.
Tools like 1Password can keep these credentials off
disk by
injecting them into the harness’s environment at runtime, but this only addresses part of the exposure: the credentials
still pass through the harness, MCP server and any intermediary processes - and so remain vulnerable to a prompt
injection attack. The four principles apply, but the MCP ecosystem makes them hard to satisfy. The most effective answer
is the first one: if you can live without an MCP server, remove it and its credentials completely. Otherwise, scope them
as tightly as possible and rotate as frequently as is practical, and assume any compromise will leak everything they
grant access to.
2. Move SSH keys to hardware
SSH keys are also an easy target for credential harvesters, and even a passphrase-protected key is vulnerable to malware
with keylogging capabilities. Migrating keys from ~/.ssh to a hardware device improves both security and convenience.
Most modern computers have a dedicated hardware enclave, or trusted platform module, that can be used to store SSH keys. This approach is more secure than storing your private key in the filesystem but comes with its own risks (keys may be wiped on BIOS updates, for example) as well as a lack of portability. A better approach is to invest in a hardware security key.
Different models are available but I am familiar with the Yubikey, which is capable of securely
generating an ECDSA keypair on-device using the in-built FIDO2
module. The public key can be
readily exported and added to your GitHub settings and authorized_keys files, while the private
key remains on the hardware device resilient to almost all forms of extraction. Using the key to
push a commit or open a session with a remote server requires both the physical presence of the security key,
and a configurable touch and/or passcode entry. It’s strongly recommended to enable at least
one of these - otherwise the security key only needs to be plugged in for an attacker with remote access to your workstation to be
able to exploit it. Whether a pincode entry is required depends on your personal threat model,
but either safeguard is significantly more user-friendly and secure than typing a long passphrase.
3. Encrypt locally stored credentials
Some secrets genuinely need to be persisted on disk - shared application secrets and third-party API tokens where providers have no federated authentication, for example. This is where encryption at rest is the next best option.
There are various options for encrypting local files. I recommend sops (Secrets OPerationS) which is an open source tool that transparently manages encryption for local files using a range of different encryption providers including PGP, age, and cloud-provider key management services such as AWS KMS and GCP KMS.
I like sops for several reasons. Firstly, it allows encryption providers to be configured on a
per-file basis. This allows you to, for example, encrypt .development.env using a
locally-stored asymmetric key while .production.env is encrypted with a symmetric AWS KMS key.
# .sops.yaml
creation_rules:
- path_regex: \.production\.env$
kms: arn:aws:kms:eu-central-1:1234567890:key/6f83c098-df70-4452-bffe-8e070f5c4972
aws_profile: production
- path_regex: \.development\.env$
age: age1yubikey1abcdefghijklmnopqrstuvwxyz # age public key, could be PGP or any other provider
Now, sops edit filename with either file will select the appropriate key, decrypt the content and open it in your
preferred $EDITOR. Another nice property is that sops encrypts secret values and inline comments, but leaves keys -
such as environment variable names - in plain text. This isn’t ideal for projects where the key names themselves could
leak some context, but for most projects keeping them unencrypted in Git is convenient for searching history and
other code archaeology purposes.
But it is also possible to use sops even more securely. One supported provider is age, a modern encryption protocol implemented in Go with several useful properties including a clean API and built-in support for post-quantum keys. Especially relevant is that it can integrate with a Yubikey via age-plugin-yubikey. This plugin makes it trivial to use the Yubikey’s PIV module to securely generate a keypair directly on the hardware. This can then be used by sops to encrypt and decrypt files as long as the Yubikey is inserted in your machine. Multiple recipients (keypairs) can also be defined per-file, which means that a team can share encrypted files in Git with each developer managing their own hardware-backed key. As with SSH keys, decryption operations can be configured to require a physical touch and/or passcode entry to suit your local threat model.
The final ingredient is direnv. A one-line .envrc script can automatically decrypt your .env
files upon entering the containing directory, load the secrets into your environment, and then unset them from your
shell upon leaving.
# .envrc
eval "$(sops -d --output-type dotenv .development.env | direnv dotenv bash /dev/stdin)"
This means secrets are never stored plain-text on disk, and are only loaded into memory in processes
launched in your application directory - already a substantial reduction in exposure. It is worth noting that
environment variables themselves are not without risk - they are readable by any process running as the same user,
accessible via the /proc/ filesystem in Linux, and are usually inherited by subprocesses. If your application supports
it direnv can also output decrypted secrets in other formats, such as JSON or YAML, which can further reduce exposure.
For me, this is the ideal development setup - the convenience and familiarity of .env files but isolated to specific
directories, with all secrets securely encrypted, and with private keys residing securely on a hardware device well out
of reach of credential harvesters.
4. Securing continuous integration
The same credential principles apply to the CI environment, but with higher stakes and the uncomfortable reality that the workflow is the direct target of credential harvesters.
CI environments have been at the centre of some of the most damaging credential harvesting attacks of 2026, for several reasons. A primary responsibility of CI workflows is to deploy, sign and release software - tasks that have traditionally been provided with highly privileged and often broadly-scoped credentials. CI environments also tend to feature frequent automated dependency installation, which provides a convenient delivery vector for malicious actors to make use of. Finally, many providers support the installation of reusable third-party workflow steps, which - especially in the case of GitHub’s Actions ecosystem - have been a factor in several high-profile security incidents.
Eliminating long-lived credentials in CI must therefore be the priority. Application secrets should not be a concern here - we have already discussed how they can be encrypted with a cloud-provider’s symmetric key which means they should never hit CI unencrypted. But two classes of credentials are commonly required: cloud provider tokens for deployment, and package repository tokens for publishing artefacts. Happily, both of these credential classes are now broadly supported by OpenID Connect (OIDC).
OIDC is a protocol based on OAuth2 that can be used to grant CI workflows access to cloud credentials and services. This is based on the concept of an ID token: a short-lived token created by and representative of a single workflow, and valid for only a few minutes. That token is presented to a downstream provider, verified, and exchanged for a short-lived API token that can be scoped to the exact permissions required by the workflow. The level of granularity depends on the provider but in principle the scoping can be as tight as “build and release a binary from this Git SHA” or “deploy a container with this tag”. Finally, the workflow uses the API token to complete its assigned task.
This process follows the same credential principles described throughout this article. There are no long-lived credentials. Those credentials that are exposed have a short lifespan, and are scoped to the minimum possible privileges the workflow requires. In the event that the credentials are compromised, there is limited opportunity to exploit them - in a properly configured environment the attacker would only be able to perform the workflow’s defined task. And they have zero value on the black market. OIDC is supported by cloud providers such as AWS, Google and Azure as well as package registries including npm and PyPI. If you want to make use of OIDC in an environment that doesn’t have first-class support you can use OpenBao (the open source fork of Hashicorp Vault) as discussed later in this article.
One caveat is that while OIDC eliminates credentials at rest it does leave short-lived credentials exposed to code running inside the CI flow, including third-party actions. As we have seen, these actions have been a regular entry point for attacks on CI workflows. Pinning such dependencies to specific SHAs helps to protect against a compromised action, and provides defence-in-depth in case an OIDC flow is misconfigured to issue tokens with excessive permissions.
5. Deploying to production
The final step in the software development lifecycle is production. The threat model is different here, and mostly outside the scope of this article. But the same principles apply. Workload identity - instance roles, task roles and pod identities - can eliminate static credentials entirely. Tightly-defined IAM scopes can reduce the blast radius if an attacker manages to get a foothold inside the system.
We saw earlier how sops can encrypt application-level secrets - that is, API keys and other credentials
that often end up in environment variables and configuration files - using a symmetric key issued by your cloud
provider. Making such secrets available in your production environment is straightforward. The exact methodology depends on your cloud
provider, but for AWS simply add the kms:Decrypt permission - scoped to the specific key ARN - to your EC2 instance
role, ECS task role or pod identity’s IAM policy.
# main.tf
resource "aws_iam_role_policy" "my_instance_role_policy" {
role = aws_iam_role.my_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "KMS"
Effect = "Allow"
Action = ["kms:Decrypt"]
Resource = aws_kms_key.my_key.arn
},
],
})
You can now call the KMS API in your bootstrap script or container entrypoint to turn your encrypted secrets into plain text ready to be injected into your application.
This approach is a good starting point, but once a project is deployed beyond a developer’s workstation, new questions emerge that Git and sops cannot answer effectively. How can access to a group of secrets be limited to a specific team? Which secrets have been accessed, when, and by whom? How can a developer obtain a short-lived credential with which to access a production database for debugging purposes? Cloud-provider secrets managers such as AWS Secrets Manager can answer some of these questions, and may be sufficient for projects that don’t require dynamic credentials or provider-agnostic tooling. But answering comprehensively calls for a dedicated vault.
It has always been necessary to ask whether vault-class tooling was worth the operational cost. In the past, the answer was often no until a project had grown substantially. But today’s threat landscape has shifted the equation in several dimensions. The consequences of unmanaged secrets are more severe, while vault features such as dynamic credentials can benefit teams at a much earlier stage, including in pre-production environments. Furthermore, with the arrival of the Linux Foundation-governed OpenBao, the accessibility and likely long-term availability of such tooling has improved. A dedicated vault should be part of the foundational architectural planning for any project.
6. Vaults
Vaults are platforms that allow the four principles of credential management to be operationalised and implemented systematically at the infrastructure and application levels. Let’s look at how this works in practice, focusing on OpenBao - the open source fork of Hashicorp Vault, announced in late 2023 after Vault’s license change.
A vault is literally a persistence layer for secrets, which means that the first principle of avoiding storage does not apply. So let’s start with the second, which is that secrets should never be stored unencrypted. This is the most basic feature of OpenBao - an encrypted, distributed secrets storage backend that exposes a key/value API for passwords, API keys and other static credentials. Configurable audit logs provide granular visibility of per-user access patterns, and access can be readily and flexibly revoked if needed. It’s also worth noting that the value of vault-class storage isn’t confined to production. Vaults can provide secure secrets at every stage of the software development lifecycle, including staging, CI and, with appropriate network configuration, development environments too.
The third principle is to minimise the lifespan of credentials wherever possible. This is where vaults offer something
distinctive with their support for dynamic credentials. To understand how this works in OpenBao, let’s take the
well-known pattern of authenticating with a PostgreSQL database. Traditionally, a database user and password would be
statically created: myapp_production:rlys3cr3t for example. Ideally it would be scoped to a single database, but would
usually have unlimited lifespan and often broad permissions on that database. The credentials would then be included in
a connection string injected in every production container and process that requires database access. In the event any
such process was compromised, the connection string would provide an attacker with an ideal outcome - indefinite,
persistent access to the database.
From the perspective of an OpenBao client, dynamic credentials look identical to static credentials and can be retrieved
by querying a key such as database/production/myapp_readonly or database/production/myapp_readwrite. But the
process of delivering the credential is completely different. In the case of a static secret, the server simply reads it from the
storage backend and returns it to the user. But when a dynamic database credential is requested, it transparently
connects to PostgreSQL using an isolated privileged role and creates a unique database user and password solely for the
use of the requesting client. The permissions can be tightly-scoped, and more importantly, only need to remain valid for
the lifespan of the process - or shorter, with automatic renewal handled by the client SDK. When the lease expires,
the vault automatically connects to PostgreSQL and drops the role. Now, if some process is compromised, there are no
long-lived credentials to exfiltrate. The threat of persistent malicious access is neutralised.
The fourth principle is that of least privilege. We see one manifestation of this in OpenBao with another example of the workload identity pattern. OpenBao provides a number of authentication flows for both human and machine users, but particularly powerful is its integration with cloud provider access management frameworks such as AWS IAM. This integration allows a specific identity, like an EC2 instance role or ECS task role, to be assigned access to a subset of secrets. The role can then authenticate with OpenBao using its own STS identity, and obtain a token granting it access to only the secrets it is entitled to - the least privileges possible and once again with a controlled lifespan.
OpenBao’s other capabilities - PKI, SSH CA, OIDC provider - extend the same patterns to other credential classes. But it is important to acknowledge that OpenBao is not a magic bullet. It is a security-critical service with a non-trivial operational footprint. Administrators must handle high-availability deployment, unsealing processes, backups, monitoring and key rotation, for example. Like with any other system, misconfiguration can still lead to the assignment of overly broad permissions. It is this operational cost that historically pushed teams to defer adoption. But OpenBao is ideally suited to supporting architectures that uphold all four credential principles addressed in this article. Vaults are no longer a deferred decision, but a foundational one.
Conclusion
In 2026, it is clear that the landscape in which we build software is changing fast. An increasing volume of credential-harvesting malware, and the unpredictable and emergent nature of AI agents, means that storing secrets in plain text on developer machines is increasingly indefensible. Supply chain attacks mean continuous integration and production environments are also at increased risk. But the tooling to harden every stage of the software development lifecycle is already available. Our challenge is to bake it into the foundation of every project.