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. 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 : ENV VAR=1Password field name 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 . Build a 6-field shell. If env has a value, put it in; if not, an empty field. 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 Update if it exists, create if it doesn't 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. Fetch the item exactly once less exposed to a mid-run re-lock 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 =}" grab the value of the field whose label matches field 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 Even if some are missing, write out what we got and call it a success 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. secrets private repo, just for me, so .mcp.json IS tracked 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 → generates ~/.config/claude/secrets.env. Open a new shell and you're done. 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. 1. update the field's value in the 1Password GUI 2. just re-run sync on each Mac 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.