We use passwords, API keys/tokens, secrets in general. We use them locally to test more frequently than we would like to admit. We store them in shell environment variables and tell ourselves we will clean them up afterwards. But that is not what always happens, is it?

The idea of this post is to present you an alternative: I will show you how to stop storing secrets locally by using the 1Password CLI to retrieve them at runtime, including a small trick for MCP servers, so you never have to paste tokens into plain JSON config files again.

Why this matters right now: Supply Chain Security

Supply chain attacks are more and more common. A layered approach, defense in depth, is recommended to reduce the chances of them happening in the first place (a topic I will cover in a future post). But unfortunately, no defense is perfect. So what do attackers do when they compromise a target? (which could be a dev’s laptop, a CICD runner, etc): secret scanning is usually one of the very first actions in the payload, followed by the leakage of those credentials. Ensuring that we have as few secrets as possible locally stored will certainly not prevent a supply chain attack, but it will definitely reduce its blast radius. If you also place decoy secrets, like canary tokens you can have a reliable way to detect the intrusion

But first, why don’t we do us all a favor and find the existing secrets that are already lying in our filesystems? Those should be removed and rotated

Hunting for Secrets

I’m walking you through two different tools I like to use for hunting secrets: Trufflehog and Praetorian.

Trufflehog

Install trufflehog on macOS

brew install trufflehog

Run it

trufflehog filesystem "$HOME"  --exclude-paths .directory-exception --no-verification --no-update   --json > ~/truffle_report.json

Warning: By default trufflehog tests all credentials to flag them as either verified or not, to reduce the noise of the output. This means you’ll be hitting several APIs, which might not be your intention, depending on the case. In case you don’t want this, add the --no-verification flag.

Praetorian

Praetorian has NoisyParker (recently deprecated, written in Go) and Titus, which is its evolution What I like about them is that they are fast and that they don’t verify secrets by default, you have to intentionally set the --validate flag to do so

NoisyParker

Install

brew install noseyparker

Run

time noseyparker scan --datastore np.db -i ".directory-exception" "$HOME"

Review report

noseyparker report --datastore np.db -f json -o findings.json

Titus

Download

curl -L https://github.com/praetorian-inc/titus/releases/latest/download/titus-darwin-arm64 -o titus
chmod +x titus

Run

./titus scan --output titus.ds --ignore .directory-exception  "$HOME"

Review report

./titus report --datastore titus.ds/ > titus_report

Comments on running these tools

Scan exception directories and don’t forget to remove reports!

For all the tools above, I explicitly ran them by passing a directory as an argument, $HOME, and also specifying an exception directory: where I don’t want the tool to scan. Why? Because on this directory, the current one, is where I’m storing the reports file. If I don’t make exceptions for these directories subsequent scans for the same tool or from another tool will pick the reports file and show it as finding, flooding the output.

For each tool above, I used a .directory-exception file containing

{current_directory}
Library/

Now this is only useful for the duration of a particular set of scans that happen in the same time window. As soon as I finish the engagement, I remove the reports, as they contain all the sensitive data that you could possibly find in the filesystem concentrated in one place!

Tools Performance

I ran each tool with time. Same parameters, here you have the results

Trufflehog
real	15m19.754s
user	104m21.040s
sys	    1m32.384s
Noisy Parker
real	0m21.951s
user	0m38.418s
sys	    0m21.639s
Titus
real	1m18.922s
user	0m54.389s
sys	    0m32.611s

1Password CLI

Once you have cleaned up all those secrets, the next question is how to stop the problem from coming back. That is where 1Password CLI comes in. I have spent some time playing with it and I’m glad I did it. There are essentially two ways to work with environment variables without storing them locally, using 1Password:

Environment (beta)

I really like this approach, as it lets you work securely without having env vars locally stored and, also, without modifying your scripts. You create the Environment, the variables and share it with your team. Once everyone you need to share with has access and they have their 1Password beta CLI installed, everyone can use them. Something particularly useful for QA/QE teams is that you can create multiple environments and by only modifying the –environment ID when you run your test, you have access to different environments like your production, pre-production by just changing the ID!

Setting up environment

  • Go to the 1Password app –> settings –> developer and enable the CLI
  • Go to Developer –> Environments –> Create/Re-use an environment.
  • Create your env variables or, even better, import your existing .env file!
  • Copy the environment id
  • Install 1Password CLI beta –> https://app-updates.agilebits.com/product_history/CLI2 (I tested on 2.35.0-beta.0)
  • Go to the shell and run any command to authenticate (eg op vault list) to ensure it works

Example

For the sake of this exercise, I created a simple bash script, op_test.sh, to grab some information from Cloudflare. This is how the script looks

#!/bin/bash

source .env

curl https://api.cloudflare.com/client/v4/accounts/{account_id}/rules/lists/{list_id}/items -H "Authorization: Bearer $auth_bearer"

If I remove the .env file, that contains the actual secret for auth_bearer and run op like this, passing my script name as argument

op run --environment {environment_ID} -- ./op_test.sh

1Password auth is triggered and I get the results, without having a local .env file
This is particularly good if you don’t want or can’t modify your existing scripts.

Secret Reference

For using Secret Reference you don’t need to enable anything (nor installing the beta CLI). You can retrieve 1Password items directly using the op CLI tool by replacing in your scripts the source .env line with the following line

my_var=$(op read "op://{vault-name}/{item-name}/{field-name}")

You only need to pass the proper secret structure within 1Password and run the script

Bonus: MCP

If you’re running local MCP (Model Context Protocol) servers, you’ve probably faced this: every server needs an API key or token, and the easiest path is dropping them into a config file or an env var, just like we have seen with other things. It works, but it is far from ideal. Let’s use what we have learned above

How it works

Instead of pointing your MCP server command at npx or uvx directly, you wrap it with op run. That command scans your env vars for op://... references, resolves them from your 1Password vault, and passes the real values into the process it launches.

A basic MCP server block looks like this:

"mcpServers": {
  "some-mcp-server": {
    "command": "op",
    "args": ["run", "--", "uvx", "some-mcp-server"],
    "env": {
      "SOME_API_URL": "op://{vault-name}/{item-name}/{url-field}",
      "SOME_API_TOKEN": "op://{vault-name}/{item-name}/{api-token-field}"
    }
  }
}

This has worked for me and it can be generalized to anything beyond uvx. If your server normally launches with npx, the fix is the same idea — just swap command to op and prepend run -- to your original args

"github": {
  "command": "op",
  "args": ["run", "--", "npx", "-y", "@modelcontextprotocol/server-github"],
  "env": {
    "GITHUB_PERSONAL_ACCESS_TOKEN": "op://{vault-name}/{item-name}/{token-field}"
  }
}

Bonus: Bash history hygiene

And I get it, you will only run a single command with a disposable API key, so storing it in 1Password seems overkill to you. For you I have the old bash trick: space before sensitive command.

Add this to your ~/.bash_profile

HISTCONTROL=ignorespace

Now, every time you want to avoid a command from being stored in the history file, only start with a space

That’s all. No more secrets in the shell. Just retrieve them when needed. Smaller blast radius if things go wrong and, as a bonus, your secrets don’t die with your laptop. Finally, a habit that costs nothing to maintain.