cd /news/developer-tools/where-the-hell-do-i-put-this-token-s… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-37588] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=↑ positive

Where the Hell Do I Put This Token? Syncing Claude Code Secrets to 3 Macs with the 1Password CLI

A developer managing three Macs solved the problem of syncing API tokens for Claude Code's MCP servers by using the 1Password CLI. Instead of storing plaintext tokens in config files or manually copying them across machines, they keep all secrets in 1Password and sync them to each Mac with the `op` CLI, ensuring security and consistency.

read10 min views1 publishedJun 24, 2026

At some point I looked up and I had three Macs.

There's a clear reason they multiplied: trying to do everything on one machine fell apart. Run a few AI jobs in the background β€” Claude Code and friends β€” and they eat memory by the fistful, and the actual work I'm supposed to be doing starts to stutter. Editor lags, browser lags, and I end up in this backwards place where "running AI stops my own work." So I split it up: the heavy AI stuff runs on a separate Mac that I ssh

into, while my main machine stays free for the actual work. Offload the AI to the Mac next to me. That's how the number of Macs crept up.

So now there's the main one on my desk, a MacBook I carry around, and the box that runs the AI β€” and the moment you want Claude Code to behave the same on all of them, the annoying part β€” the part that quietly eats your time β€” is dealing with tokens. Running the MCP servers for Notion, Linear, and GitHub needs API tokens. Write those straight into .mcp.json

and you've got plaintext sitting in a config file. Copy-paste them across three machines by hand and, well, that's its own kind of misery.

"Where the hell do I actually put this token?" I went back and forth on that for a while, and eventually landed on 1Password, which I already use every day. The short version: keep every secret in one place β€” 1Password β€” and sync it to each Mac with the 1Password CLI ( op). That killed both "plaintext scattered everywhere" and "paste it N times for N machines" in one shot.

Below is what was painful, why I went with the 1Password CLI, and exactly what the two scripts I actually run look like β€” in the order I did things.

Once you start using Claude Code for real, your MCP server config (.mcp.json

) needs tokens. At first I just wrote the values in directly.

{
  "mcpServers": {
    "github": {
      "headers": { "Authorization": "Bearer ghp_xxxxxxxxxxxx" }  // ← plaintext
    }
  }
}

This had a pile of problems:

git

history just feels gross.And the thing I agonized over most: where do I store the token in the first place? Hand a .env

to each machine? Encrypt it into my dotfiles? Use a cloud secrets service? Every option felt like "add a whole new mechanism," and I kept not pulling the trigger.

Then it hit me one day: the answer was already sitting right there. I've kept my passwords and credit cards in 1Password for years. A token is just another "secret" β€” there's no reason to stand up a separate home for it.

On top of that, 1Password has a CLI (op

), and you can read items out with op item get

. There's even an MCP server for it now, so the ecosystem has legs. When I lined the options up side by side, 1Password fit my use case the most naturally.

Storage method Sharing Security Verdict
Hand a .env to each machine
manual file sits in plaintext ❌ work per machine + paste mistakes
Encrypt into dotfiles via git need to manage a decryption key separately ❌ another moving part
Cloud secrets service via API good β–³ new tool to adopt, more login juggling
1Password CLI
sync with op
Touch ID + Vault βœ… picked it: just an extension of what I already use

Three things sold me. I already use it daily, so there's barely anything new to learn. I can approve with Touch ID. I can keep all my secrets in one spot. Basically, "get it done with the tools you already have" was the least-effort path.

The core of the whole thing is making the flow of secrets one-directional. The real values live only in 1Password, and from there they flow downstream only β€” into each Mac's env vars, then into config-file references.

1Password (the only source)
   β”‚  op item get (sync script)
   β–Ό
~/.config/claude/secrets.env  ← auto-generated, not tracked by git, chmod 600
   β”‚  source (loaded at shell startup)
   β–Ό
env vars  $GITHUB_MCP_PAT etc.
   β”‚  ${VAR} reference
   β–Ό
.mcp.json / settings.json  ← tracked by git. holds no real values, references only

The key point: never write a real token into a file that git tracks. .mcp.json

only gets an env-var reference like ${GITHUB_MCP_PAT}

, and the real thing is poured in from 1Password. That way you can commit the config files and no secret leaks.

Holding the whole thing up are just two scripts β€” one for saving, one for reading.

Before running the scripts, get this much out of the way:

op

)brew install 1password-cli

)op

runs with Touch ID.Claude Code MCP

(category: Password) in the Personal

vault, and add a field per token.On the script side, I map env var names to 1Password field names like this (shared by both scripts):

declare -a MAP=(
  "NOTION_API_KEY=notion_api_key"
  "LINEAR_API_KEY=linear_api_key"
  "GITHUB_MCP_PAT=github_pat"
  "SLACK_MCP_XOXB_TOKEN=slack_xoxb"
  "GOOGLE_TOKEN_JSON=google_token_json"
  "STRIPE_SECRET_KEY=stripe_secret_key"
)

The vault and item can be overridden with env vars (OP_VAULT

/ OP_ITEM

). The defaults are Personal

/ Claude Code MCP

.

First, the script that saves the tokens currently in my shell into 1Password β€” save-to-1password.sh

. The trick is that it grabs the values from the export

s in ~/.zshrc

and never spells them out on the command line directly (so they don't land in your history).

The heart of it is how you assemble the fields you hand to 1Password. You line them up in the form ${field name}[password]=value

, and if the item already exists you use op item edit

, otherwise op item create

.

fields=()
for pair in "${MAP[@]}"; do
  var="${pair%%=*}"; field="${pair#*=}"
  val="${!var-}"
  if [ -z "$val" ]; then
    fields+=( "${field}[password]=" )         # just create an empty slot
  else
    fields+=( "${field}[password]=${val}" )    # put the value in
  fi
done

if op item get "$ITEM" --vault "$VAULT" >/dev/null 2>&1; then
  op item edit "$ITEM" --vault "$VAULT" "${fields[@]}" >/dev/null
else
  op item create --category=password --title="$ITEM" --vault "$VAULT" "${fields[@]}" >/dev/null
fi

The quietly useful bit is creating an empty slot for fields that have no value. You don't need all six tokens lined up from day one. Create the empty fields up front, and later you just paste the post-rotation value into the 1Password GUI to fill them. Instead of going for perfection in one go, I made it something I could migrate to bit by bit.

Run it from an interactive terminal (since Touch ID approval is needed). From inside Claude Code you can just hit it with a leading !

.

! bash ~/claudecode/scripts/save-to-1password.sh

Next, the real workhorse, the one I run on each Mac β€” sync-claude-secrets.sh

. It reads the tokens out of 1Password and writes them to an env file that git doesn't track. Run this on each Mac and every machine ends up with the same values. This is exactly what I was after.

I got bitten here once. At first I was calling op read

per field, but partway through 1Password would re-lock and ask me for Touch ID over and over, or some fields would just fail to fetch. So I switched to grabbing the whole item at once with --format json

and pulling each field out with jq

. One op

call, total.

json="$(op item get "$ITEM" --vault "$VAULT" --format json)"

umask 177  # generated file will be 600
got=0; miss=()
for pair in "${MAP[@]}"; do
  var="${pair%%=*}"; field="${pair#*=}"
  val="$(printf '%s' "$json" | jq -r --arg f "$field" \
        '.fields[] | select(.label==$f) | .value // empty')"
  if [ -z "$val" ]; then
    miss+=("$field"); continue
  fi
  printf 'export %s=%q\n' "$var" "$val" >> "$tmp"
  got=$((got+1))
done

The output goes to ~/.config/claude/secrets.env

. I use umask 177

and chmod 600

so only the owner can read it, and the values get shell-safe-escaped with printf '%q'

.

I also made it fail if nothing came through, but succeed even if some pieces are missing β€” fail-soft.

if [ "$got" -eq 0 ]; then
  echo "βœ— Got nothing. Check your field names." >&2
  exit 1
fi

So even in a state like "haven't added the Stripe token yet," the sync still goes through. Whatever's filled in lands in env, and a warning tells you what's missing. Once I dropped the "nothing works unless everything's there" rule, the day-to-day got a lot easier.

Once the env is built, open a new shell or source ~/.config/claude/secrets.env

to apply it.

Once the values are in env, all that's left is for .mcp.json

to receive them via ${VAR}

.

{
  "mcpServers": {
    "github": {
      "headers": { "Authorization": "Bearer ${GITHUB_MCP_PAT}" }
    }
  }
}

No real value is written anywhere, so this file commits just fine. Alongside that, I use .gitignore

to broadly exclude the files where secrets tend to sneak in.

CLAUDE.local.md
**/.env
**/.env.*
*.pem
*.key

Once the setup was in place, the daily operations got almost anticlimactically simple.

Setting up a new Mac is just: turn on the op

integration and run sync once.

bash ~/claudecode/scripts/sync-claude-secrets.sh

Rotating a token is: update the one item in 1Password, then re-run sync on each Mac. You don't touch the config files at all.

bash ~/claudecode/scripts/sync-claude-secrets.sh

All that changed was "fix every machine by hand" becoming "fix 1Password once and sync on each machine." But honestly, even just that made a real difference to how much it weighs on me.

A few small traps before it ran cleanly:

unbound variable

. Wrap it in braces like ${ITEM}

to avoid it.op

from a non-interactive shell and the approval dialog never shows, so auth fails. Always run save and sync from an interactive terminal.⚠️

Heads up on git history and rotation

If there was ever a period where you wrote plaintext tokens into config files,the old values are still sitting in your git history.Swapping in env references doesn't erase history, so after migrating it's safest torotate the affected tokensjust in case (update both the service side and 1Password).

After consolidating all my secrets into 1Password, the things that had been bugging me forever β€” "plaintext scattered everywhere," "paste it per machine," "fix every machine on every rotation" β€” all went away together. What it actually does is plain bash: op item get

, jq

, and writing out export

s. Zero flashiness. But that one principle β€” narrow the source of secrets down to a single place β€” really does pull its weight.

The more MCP servers you add, the more kinds of tokens you handle. Reusing a password manager you already trust as the "single source of keys" is, I think, a pretty practical move β€” you get there without adding a new mechanism.

So β€” where do you keep your Claude Code tokens? If you've got a better way, I'd genuinely like to hear it. And if you're still writing them straight into config files, start by getting just one of them out into 1Password.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/where-the-hell-do-i-…] indexed:0 read:10min 2026-06-24 Β· β€”