# Secrets

> How tokens stay out of plain text, both in the repo and in your config files.

## The rule

aisync writes a token in plain text in neither place:

- **The repo**: tokens are only in `secrets.json`, encrypted, and the setup holds `{{secret:name}}` references.
- **This machine's config**: `.claude.json` holds references too. The real value is handed to the MCP server process when it starts.

## The encrypted store

`secrets.json` is encrypted with your passphrase.

- Key: derived from the passphrase with PBKDF2-SHA256 (600,000 rounds).
- Cipher: AES-256-GCM, each entry sealed separately and bound to its name, so moving ciphertext under another name doesn't decrypt.
- The key is derived once per machine and kept in the macOS keychain, or in `~/.config/aisync/keys` as a 0600 file on Linux.

A leaked repo shows no tokens without the passphrase. A short passphrase can still be guessed with enough time, so make it long.

## How an MCP server gets its token

On pull, aisync doesn't decrypt anything into the config. It rewrites the server instead.

A server started by a command is wrapped in `aisync exec`:

```json
"slack": {
  "command": "/Users/you/.local/bin/aisync",
  "args": ["exec", "--repo", "you/aisync-config", "--", "npx", "-y", "@modelcontextprotocol/server-slack"],
  "env": { "SLACK_BOT_TOKEN": "{{secret:slack-bot}}" }
}
```

When Claude Code starts it, aisync resolves the references and runs the original command. The value exists only in that server process.

An HTTP server that authenticates with a header uses Claude Code's `headersHelper`:

```json
"github": {
  "type": "http",
  "url": "https://api.githubcopilot.com/mcp/",
  "headersHelper": "/Users/you/.local/bin/aisync headers --repo you/aisync-config 'Authorization=Bearer {{secret:github-auth}}'"
}
```

Claude Code runs it on every connection to get the headers.

`aisync exec` and `aisync headers` don't use the network. They read the copy of `secrets.json` saved on the last pull (still encrypted) and the key in the keychain.

## Tokens inside a URL

A server whose URL carries a token (`https://…/mcp?token=…`) can't be handled without plain text, so it is refused. Switch it to header authentication, or leave it out with `capture --server`.

## Tokens in files (BLOCKS)

settings.json, CLAUDE.md and skill files are uploaded as they are, so a token in them stops the upload.

Claude Code has no other channel for a token in settings.json `env`. Pick one:

- If the token is for an MCP server, move it to that server's `env`. It becomes encrypted.
- If Claude as a whole needs the variable, remove it from settings.json, put `"env": {"NAME": "{{secret:name}}"}` in the profile's `profile.json`, and start Claude with `aisync run <profile>`. Claude and every command it runs can then see that value.

## Managing tokens

```sh
aisync secret list                 # names only
aisync secret set github-token     # value is prompted for, or read from stdin
aisync secret rm github-token
aisync secret prune --dry-run      # tokens no profile uses
aisync passwd                      # change the passphrase
aisync unlock                      # enter the passphrase on this machine again
```

`passwd` re-encrypts the whole store. Other machines need `aisync unlock` once after their next fetch before their MCP servers start again.

## What scan looks for

Known formats (Slack `xoxb-`, GitHub `ghp_` and `github_pat_`, OpenAI and Anthropic `sk-`, Vault `hvs.`, AWS `AKIA` and more), and values that look random under names containing TOKEN, SECRET, KEY, PASSWORD or AUTH. It is rule-based and can miss new formats, so read the scan output before you upload.

Claude Code keeps a few backups of `.claude.json` in `~/.claude/backups/`. They can still hold the plain-text tokens from before aisync, and scan reports them as `local`. Claude Code rotates old backups out on its own.
