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.jsonholds 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/keysas 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:
"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:
"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'sprofile.json, and start Claude withaisync run <profile>. Claude and every command it runs can then see that value.
Managing tokens
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.
These docs describe aisync 0.1.