{"slug": "where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with", "title": "Where the Hell Do I Put This Token? Syncing Claude Code Secrets to 3 Macs with the 1Password CLI", "summary": "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.", "body_md": "At some point I looked up and I had three Macs.\n\nThere'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`\n\ninto, 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.\n\nSo 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`\n\nand 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.\n\n\"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.\n\nBelow 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.\n\nOnce you start using Claude Code for real, your MCP server config (`.mcp.json`\n\n) needs tokens. At first I just wrote the values in directly.\n\n```\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"headers\": { \"Authorization\": \"Bearer ghp_xxxxxxxxxxxx\" }  // ← plaintext\n    }\n  }\n}\n```\n\nThis had a pile of problems:\n\n`git`\n\nhistory just feels gross.And the thing I agonized over most: **where do I store the token in the first place?** Hand a `.env`\n\nto 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.\n\nThen 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.\n\nOn top of that, 1Password has a CLI (`op`\n\n), and you can read items out with `op item get`\n\n. 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.\n\n| Storage method | Sharing | Security | Verdict |\n|---|---|---|---|\nHand a `.env` to each machine |\nmanual | file sits in plaintext | ❌ work per machine + paste mistakes |\n| Encrypt into dotfiles | via git | need to manage a decryption key separately | ❌ another moving part |\n| Cloud secrets service | via API | good | △ new tool to adopt, more login juggling |\n1Password CLI |\nsync with `op`\n|\nTouch ID + Vault | ✅ picked it: just an extension of what I already use |\n\nThree 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.\n\nThe 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.\n\n```\n1Password (the only source)\n   │  op item get (sync script)\n   ▼\n~/.config/claude/secrets.env  ← auto-generated, not tracked by git, chmod 600\n   │  source (loaded at shell startup)\n   ▼\nenv vars  $GITHUB_MCP_PAT etc.\n   │  ${VAR} reference\n   ▼\n.mcp.json / settings.json  ← tracked by git. holds no real values, references only\n```\n\nThe key point: **never write a real token into a file that git tracks.** `.mcp.json`\n\nonly gets an env-var reference like `${GITHUB_MCP_PAT}`\n\n, and the real thing is poured in from 1Password. That way you can commit the config files and no secret leaks.\n\nHolding the whole thing up are just two scripts — one for saving, one for reading.\n\nBefore running the scripts, get this much out of the way:\n\n`op`\n\n)`brew install 1password-cli`\n\n)`op`\n\nruns with Touch ID.`Claude Code MCP`\n\n(category: Password) in the `Personal`\n\nvault, 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):\n\n```\n# ENV_VAR=1Password field name\ndeclare -a MAP=(\n  \"NOTION_API_KEY=notion_api_key\"\n  \"LINEAR_API_KEY=linear_api_key\"\n  \"GITHUB_MCP_PAT=github_pat\"\n  \"SLACK_MCP_XOXB_TOKEN=slack_xoxb\"\n  \"GOOGLE_TOKEN_JSON=google_token_json\"\n  \"STRIPE_SECRET_KEY=stripe_secret_key\"\n)\n```\n\nThe vault and item can be overridden with env vars (`OP_VAULT`\n\n/ `OP_ITEM`\n\n). The defaults are `Personal`\n\n/ `Claude Code MCP`\n\n.\n\nFirst, the script that saves the tokens currently in my shell into 1Password — `save-to-1password.sh`\n\n. The trick is that it grabs the values from the `export`\n\ns in `~/.zshrc`\n\nand **never spells them out on the command line directly** (so they don't land in your history).\n\nThe heart of it is how you assemble the fields you hand to 1Password. You line them up in the form `${field name}[password]=value`\n\n, and if the item already exists you use `op item edit`\n\n, otherwise `op item create`\n\n.\n\n```\n# Build a 6-field shell. If env has a value, put it in; if not, an empty field.\nfields=()\nfor pair in \"${MAP[@]}\"; do\n  var=\"${pair%%=*}\"; field=\"${pair#*=}\"\n  val=\"${!var-}\"\n  if [ -z \"$val\" ]; then\n    fields+=( \"${field}[password]=\" )         # just create an empty slot\n  else\n    fields+=( \"${field}[password]=${val}\" )    # put the value in\n  fi\ndone\n\n# Update if it exists, create if it doesn't\nif op item get \"$ITEM\" --vault \"$VAULT\" >/dev/null 2>&1; then\n  op item edit \"$ITEM\" --vault \"$VAULT\" \"${fields[@]}\" >/dev/null\nelse\n  op item create --category=password --title=\"$ITEM\" --vault \"$VAULT\" \"${fields[@]}\" >/dev/null\nfi\n```\n\nThe 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.\n\nRun it from an interactive terminal (since Touch ID approval is needed). From inside Claude Code you can just hit it with a leading `!`\n\n.\n\n```\n! bash ~/claudecode/scripts/save-to-1password.sh\n```\n\nNext, the real workhorse, the one I run on each Mac — `sync-claude-secrets.sh`\n\n. 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.\n\nI got bitten here once. At first I was calling `op read`\n\nper 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`\n\nand pulling each field out with `jq`\n\n. One `op`\n\ncall, total.\n\n```\n# Fetch the item exactly once (less exposed to a mid-run re-lock)\njson=\"$(op item get \"$ITEM\" --vault \"$VAULT\" --format json)\"\n\numask 177  # generated file will be 600\ngot=0; miss=()\nfor pair in \"${MAP[@]}\"; do\n  var=\"${pair%%=*}\"; field=\"${pair#*=}\"\n  # grab the value of the field whose label matches `field`\n  val=\"$(printf '%s' \"$json\" | jq -r --arg f \"$field\" \\\n        '.fields[] | select(.label==$f) | .value // empty')\"\n  if [ -z \"$val\" ]; then\n    miss+=(\"$field\"); continue\n  fi\n  printf 'export %s=%q\\n' \"$var\" \"$val\" >> \"$tmp\"\n  got=$((got+1))\ndone\n```\n\nThe output goes to `~/.config/claude/secrets.env`\n\n. I use `umask 177`\n\nand `chmod 600`\n\nso **only the owner can read it**, and the values get shell-safe-escaped with `printf '%q'`\n\n.\n\nI also made it **fail if nothing came through, but succeed even if some pieces are missing** — fail-soft.\n\n```\nif [ \"$got\" -eq 0 ]; then\n  echo \"✗ Got nothing. Check your field names.\" >&2\n  exit 1\nfi\n# Even if some are missing, write out what we got and call it a success\n```\n\nSo 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.\n\nOnce the env is built, open a new shell or `source ~/.config/claude/secrets.env`\n\nto apply it.\n\nOnce the values are in env, all that's left is for `.mcp.json`\n\nto receive them via `${VAR}`\n\n.\n\n```\n{\n  \"mcpServers\": {\n    \"github\": {\n      \"headers\": { \"Authorization\": \"Bearer ${GITHUB_MCP_PAT}\" }\n    }\n  }\n}\n```\n\nNo real value is written anywhere, so this file commits just fine. Alongside that, I use `.gitignore`\n\nto broadly exclude the files where secrets tend to sneak in.\n\n```\n# secrets (private repo, just for me, so .mcp.json IS tracked)\nCLAUDE.local.md\n**/.env\n**/.env.*\n*.pem\n*.key\n```\n\nOnce the setup was in place, the daily operations got almost anticlimactically simple.\n\n**Setting up a new Mac** is just: turn on the `op`\n\nintegration and run sync once.\n\n```\nbash ~/claudecode/scripts/sync-claude-secrets.sh\n# → generates ~/.config/claude/secrets.env. Open a new shell and you're done.\n```\n\n**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.\n\n```\n# 1. update the field's value in the 1Password GUI\n# 2. just re-run sync on each Mac\nbash ~/claudecode/scripts/sync-claude-secrets.sh\n```\n\nAll 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.\n\nA few small traps before it ran cleanly:\n\n`unbound variable`\n\n. Wrap it in braces like `${ITEM}`\n\nto avoid it.`op`\n\nfrom a non-interactive shell and the approval dialog never shows, so auth fails. Always run save and sync from an interactive terminal.⚠️\n\nHeads up on git history and rotation\n\nIf 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).\n\nAfter 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`\n\n, `jq`\n\n, and writing out `export`\n\ns. Zero flashiness. But that one principle — **narrow the source of secrets down to a single place** — really does pull its weight.\n\nThe 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.\n\nSo — 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.", "url": "https://wpnews.pro/news/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with", "canonical_source": "https://dev.to/coa00/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with-the-1password-cli-3ljl", "published_at": "2026-06-24 10:28:36+00:00", "updated_at": "2026-06-24 10:43:41.816310+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "large-language-models"], "entities": ["Claude Code", "1Password", "1Password CLI", "GitHub", "Notion", "Linear", "MCP"], "alternates": {"html": "https://wpnews.pro/news/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with", "markdown": "https://wpnews.pro/news/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with.md", "text": "https://wpnews.pro/news/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with.txt", "jsonld": "https://wpnews.pro/news/where-the-hell-do-i-put-this-token-syncing-claude-code-secrets-to-3-macs-with.jsonld"}}