Stop Storing Secrets on Disk: A Developer's Guide to 1Password CLI and SSH Agent
Here is what a malicious npm post-install script does when it runs on a machine that uses 1Password's SSH agent and op run for secret injection: it greps ~/.ssh/id_rsa, ~/.aws/credentials, and ~/.config and finds nothing. Not obfuscated secrets. Not encrypted blobs it might crack later. Nothing. The paths are empty or absent because the credentials never landed there.
That is the threat model shift this tutorial is actually about. The 2023 node-ipc and event-source-polyfill supply chain incidents used exactly that grep pattern — post-install scripts scanning home directories for cleartext keys and tokens. The defense is not better intrusion detection. It is removing the files the attacker is looking for.
The Landscape That Made This Necessary
The developer credential story has been embarrassing for a long time. The canonical workflow looks like this: a senior engineer DMing a .env file over Slack to a new hire, who saves it to ~/projects/app/.env and forgets about it. That file might contain database passwords, AWS access keys, and Stripe secret keys, all in plaintext, on a laptop that connects to airport WiFi, runs dozens of npm packages with post-install scripts, and will eventually be sold or lost.
SSH keys have the same problem at a different layer. Most developers generate id_ed25519, store it in ~/.ssh/, and optionally protect it with a passphrase they type once and then cache for eight hours via ssh-agent. If that machine is compromised — or if a dependency's post-install script has read access to the home directory — the key is available.
The tools to fix this have existed for years. HashiCorp Vault with agent sidecar injection handles the unattended automation case well, but its operational overhead — cluster management, policy authoring, token renewal — is prohibitive for most engineering teams. AWS Secrets Manager solves the bootstrap secret problem elegantly if your entire stack runs on AWS, because the root credential is the EC2 instance identity. But it locks you to one cloud and provides no help for local development workflows where the developer, not an instance, is the identity.
1Password's approach targets the specific case where those alternatives break down: multi-machine, multi-cloud developer workflows where a human is present and biometric unlock is available. The 1Password 8 desktop app introduced an SSH agent. The op CLI, now at a mature feature set, introduced URI-based secret references. Together they close the plaintext-on-disk problem for the three workflows that create the most exposure.
Three Workflows, Zero Plaintext Files
SSH Authentication Through the 1Password Agent
The 1Password desktop app runs a local SSH agent bound to a Unix socket at ~/.1password/agent.sock. Routing your SSH client through it requires exactly one configuration block:
# ~/.ssh/config
Host *
IdentityAgent ~/.1password/agent.sock
When ssh initiates a connection, it asks the 1Password agent to sign the server's challenge using the private key. The agent prompts for biometric confirmation (Touch ID on macOS, Windows Hello, or a PIN on Linux), performs the signing operation inside its encrypted memory space, and returns only the signature. The private key is never written to ~/.ssh/id_rsa or any other path on disk. There is no key file to exfiltrate.
One practical note on the socket path: ~/.1password/agent.sock is the default on macOS and most Linux configurations, but on systems following XDG base directory conventions, the socket may live under $XDG_RUNTIME_DIR. If you copy this config verbatim and SSH auth works on macOS but fails inside a Ubuntu dev container, this is almost certainly why. Check ls ~/.1password/ and ls $XDG_RUNTIME_DIR/1password/ before assuming the agent is broken.
To add a key, open 1Password, navigate to an SSH Key item, and click "Add to SSH Agent." The agent stores and manages the key. You can have multiple keys for multiple identities — personal GitHub, work GitLab, production servers — without any of them touching the filesystem.
Replacing .env Files with Vault References
op run is the mechanism that makes .env files safe. The workflow is:
- Your
.envfile contains references, not secrets:
DATABASE_URL=op://Production/Postgres/connection-string
STRIPE_SECRET_KEY=op://Production/Stripe/secret-key
AWS_ACCESS_KEY_ID=op://Infrastructure/AWS/access-key-id
AWS_SECRET_ACCESS_KEY=op://Infrastructure/AWS/secret-access-key
- You launch your application through the CLI:
op run --env-file=.env -- node server.js
At launch, op run resolves each op://vault/item/field URI against your vault, injects the real values as environment variables into the subprocess, then discards them when the process exits. The .env file itself is a list of pointers. You can commit it to a private repository without exposing credentials.
The URI format is op://vault-name/item-name/field-name. Vault and item names map directly to what you see in the 1Password UI. The CLI authenticates using your desktop app session (biometric on your development machine) or a service account token in unattended environments.
For local development with frameworks like Next.js or Vite that expect to read .env directly at startup, you wrap the dev command: op run --env-file=.env -- npm run dev. The framework sees the resolved environment variables without knowing they came from a vault.
Git SSH Commit Signing Without GPG
GPG key management is notoriously painful: expiring subkeys, keyserver synchronization, cross-machine import/export, the ~/.gnupg directory that accumulates state across years. Git's SSH commit signing, introduced in Git 2.34, eliminates this entirely when combined with a vault-stored key.
Configure Git to use SSH signing:
git config --global gpg.format ssh
git config --global commit.gpgsign true
git config --global user.signingkey "key::ssh-ed25519 AAAA..."
The public key in user.signingkey can be retrieved from 1Password and placed here without risk. When you commit, Git asks the 1Password SSH agent to sign the commit object. The agent prompts for biometric confirmation. The private key never leaves the vault.
For verification, configure your allowed signers file:
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
And populate it with the email-to-public-key mappings for contributors whose commits you want to verify. This is the equivalent of a GPG trust store but simpler to reason about.
The CI caveat matters here: Git SSH commit signing with a vault key requires the 1Password desktop app running and unlocked on the signing machine. Automated CI commit signing — release bots, changelog generators, anything that calls git commit in a pipeline — will silently fall back to unsigned commits if the agent socket is absent. If your repository enforces signed commits via branch protection rules, this produces an authentication failure that looks like a permission error, not a missing agent. Plan for this before enabling enforcement.
The Non-Obvious Benefit: Your .env Becomes Infrastructure Documentation
The security argument for vault-backed secrets is real and the threat model is valid. But the largest unreported benefit for most teams is operational, not security-related.
When every value in your .env is an op:// URI, the file becomes self-documenting infrastructure. A new engineer who clones the repository and runs op run --env-file=.env -- npm run dev gets an immediate, precise error if any vault items are missing:
[ERROR] 2026/07/03 09:14:22 op://Production/Postgres/connection-string: item not found in vault "Production"
Compare that to the alternative: the classic ritual where the new hire realizes after thirty minutes that the app won't start because a DATABASE_URL is missing, then asks in Slack, then waits for someone to share "the real .env" — which travels through message history where it lives unencrypted indefinitely.
The .env with URI references tells you exactly which vault items are required. A vault access provisioning step that grants the new hire access to the Production and Infrastructure vaults before day one becomes the natural onboarding checklist item. This is an improvement in security posture — credentials do not flow through Slack — but it is also an improvement in developer experience. The error is actionable rather than mysterious.
This reframing matters for getting teams to adopt the workflow. "We are doing this for security" is an uphill sell against developer inertia. "We are doing this so onboarding takes thirty minutes instead of three hours" lands differently.
What Actually Breaks in Production (And How to Plan for It)
Honest adoption of this workflow requires confronting three operational realities.
The bootstrap secret problem. op run in CI requires a 1Password service account token to authenticate with the vault. That token is itself a secret that must be stored somewhere — almost certainly as a GitHub Actions secret or equivalent. This does not undermine the approach, but it means you have traded N plaintext secrets for one privileged token with its own rotation discipline. Document this root-of-trust dependency explicitly. If the service account token expires, vault resolution fails in a way that produces permission errors rather than clear "token expired" messages, and pipelines grind to a halt in ways that are confusing to debug at 2 AM.
Long-lived processes and secret rotation. op run resolves secrets at process start, not on access. A Node.js server that forks worker processes after startup passes the already-resolved environment variables to those forks — which is correct behavior, but it means that rotating a compromised credential requires a full process restart. Under incident pressure, teams frequently rotate the vault item and assume the running process picks up the change. It does not. Build runbooks that explicitly include "restart the application" as a step following credential rotation.
Containers and headless environments. Any container that needs runtime secrets must either embed the op binary and a service account token in the image, or use a sidecar pattern. The service account token approach adds image size and a secret-in-image risk if the image is ever pushed to a public registry by mistake. The sidecar pattern adds startup latency that compounds in Kubernetes environments with many replicas. For teams running at that scale, HashiCorp Vault with dynamic secret leases is worth the operational overhead — it was designed for the unattended, high-scale case that 1Password's approach does not optimize for.
The calculus is clear: 1Password's SSH agent and op run are the right tool when a human with biometric authentication is always in the loop. The moment you need fully headless, unattended secret injection at scale, you are managing a service account and the security model resembles what you had before, minus the plaintext-on-disk problem.
What to Actually Do with This
For individual developers and small teams, the adoption path is straightforward:
- Install the 1Password desktop app (version 8 or later) and the
opCLI. Enable the SSH agent in Settings → Developer → SSH Agent. - Add
IdentityAgent ~/.1password/agent.sockto~/.ssh/config. Remove existing key files from~/.ssh/— or at minimum revoke them from services and stop using them. - Audit your
.envfiles. For each plaintext value, create the corresponding item in 1Password and replace the value with anop://URI. - Wrap your dev startup commands:
op run --env-file=.env -- <your-command>. - If your team uses commit signing, configure
gpg.format sshandcommit.gpgsign true, using a vault-stored ed25519 key. - For CI, create a 1Password service account with read access to only the vaults required by that pipeline. Store the service account token as the one bootstrap secret in your CI provider. Audit its expiration and set a calendar reminder.
The onboarding improvement arrives automatically: the next engineer you hire will clone the repo, run the startup command, see clear errors for any missing vault items, and know exactly what access to request.
The Actual Takeaway
Vault-backed credential storage is the correct answer to the post-install script exfiltration pattern. Not because it is theoretically sound, but because it is the specific defense against the specific attack that compromised real packages in 2023. Empty ~/.ssh/ and ~/.aws/ directories are not a side effect of good operational hygiene — they are the goal.
The workflow 1Password has assembled — local SSH agent, URI-based secret references, SSH commit signing — is mature enough to deploy today with a clear understanding of its limits. It solves the developer-machine problem. It does not solve the unattended-container problem. Know the boundary, design your production systems accordingly, and stop shipping .env files over Slack.
Sources & Editorial Disclosure
This article was researched and written with AI assistance (Claude by Anthropic) as part of StackRadar's automated editorial pipeline. Content was synthesised from the following public developer community sources: Dev.to.
All technical claims, version numbers, benchmarks, and project details should be independently verified against official documentation or the original sources listed above. StackRadar analyses and synthesises publicly available information and does not claim original authorship of the underlying events, projects, or research described. Mention of any project, product, or organisation does not constitute an endorsement by StackRadar. This content is provided for informational purposes only — 2026-07-03.