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

> 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: 2026-06-24 10:28:36+00:00

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.
