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-verificationflag.
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.