Sync global opencode configuration across machines via a GitHub repo, with optional secrets support for private repos.
- Syncs global opencode config (
~/.config/opencode) and related directories - Optional secrets sync when the repo is private
- Optional session sync to share conversation history across machines
- Optional prompt stash sync to share stashed prompts and history across machines
- Startup auto-sync with restart toast
- Per-machine overrides via
opencode-synced.overrides.jsonc - Custom
/sync-*commands andopencode_synctool
- GitHub CLI (
gh) installed and authenticated (gh auth login) - Git installed and available on PATH
Enable the plugin in your global opencode config (opencode will install it on next run):
opencode does not auto-update plugins. To update, modify the version number in your config file.
Run /sync-init to create a new sync repo:
- Detects your GitHub username
- Creates a private repo (
my-opencode-configby default) - Clones the repo and pushes your current config
Run /sync-link to connect to your existing sync repo:
- Searches your GitHub for common sync repo names (prioritizes
my-opencode-config) - Clones and applies the synced config
- Overwrites local config with synced content (preserves your local overrides file)
If auto-detection fails, specify the repo name: /sync-link my-opencode-config
After linking, restart opencode to apply the synced settings.
You can specify a custom repo name or use an organization:
/sync-init- Uses{your-username}/my-opencode-config/sync-init my-config- Uses{your-username}/my-config/sync-init my-org/team-config- Usesmy-org/team-config
Manual configuration
Create ~/.config/opencode/opencode-synced.jsonc:
{
"repo": {
"owner": "your-org",
"name": "opencode-config",
"branch": "main",
},
"includeSecrets": false,
"includeMcpSecrets": false,
"includeSessions": false,
"includePromptStash": false,
"extraSecretPaths": [],
}~/.config/opencode/opencode.jsonandopencode.jsonc~/.config/opencode/AGENTS.md~/.config/opencode/agent/,command/,mode/,tool/,themes/,plugin/
Enable secrets with /sync-enable-secrets or set "includeSecrets": true:
~/.local/share/opencode/auth.json~/.local/share/opencode/mcp-auth.json- Any extra paths in
extraSecretPaths(allowlist)
MCP API keys stored inside opencode.json(c) are not committed by default. To allow them
in a private repo, set "includeMcpSecrets": true (requires includeSecrets).
Sync your opencode sessions (conversation history from /sessions) across machines by setting "includeSessions": true. This requires includeSecrets to also be enabled since sessions may contain sensitive data.
{
"repo": { ... },
"includeSecrets": true,
"includeSessions": true
}Synced session data:
~/.local/share/opencode/storage/session/- Session files~/.local/share/opencode/storage/message/- Message history~/.local/share/opencode/storage/part/- Message parts~/.local/share/opencode/storage/session_diff/- Session diffs
Sync your stashed prompts and prompt history across machines by setting "includePromptStash": true. This requires includeSecrets to also be enabled since prompts may contain sensitive data.
{
"repo": { ... },
"includeSecrets": true,
"includePromptStash": true
}Synced prompt data:
~/.local/state/opencode/prompt-stash.jsonl- Stashed prompts~/.local/state/opencode/prompt-history.jsonl- Prompt history
Create a local-only overrides file at:
~/.config/opencode/opencode-synced.overrides.jsonc
Overrides are merged into the runtime config and re-applied to opencode.json(c) after pull.
If your opencode.json(c) contains MCP secrets (for example mcp.*.headers or mcp.*.oauth.clientSecret), opencode-synced will automatically:
- Move the secret values into
opencode-synced.overrides.jsonc(local-only). - Replace the values in the synced config with
{env:...}placeholders.
This keeps secrets out of the repo while preserving local behavior. On other machines, set the matching environment variables (or add local overrides).
If you want MCP secrets committed (private repos only), set "includeMcpSecrets": true alongside "includeSecrets": true.
Env var naming rules:
- If the header name already looks like an env var (e.g.
CONTEXT7_API_KEY), it is used directly. - Otherwise:
opencode_mcp_<SERVER>_<HEADER>(non-alphanumerics become_). - OAuth client secrets use
opencode_mcp_<SERVER>_OAUTH_CLIENT_SECRET.
| Command | Description |
|---|---|
/sync-init |
Create a new sync repo (first machine) |
/sync-link |
Link to existing sync repo (additional machines) |
/sync-status |
Show repo status and last sync times |
/sync-pull |
Fetch and apply remote config |
/sync-push |
Commit and push local changes |
/sync-enable-secrets |
Enable secrets sync (private repos only) |
/sync-resolve |
Auto-resolve uncommitted changes using AI |
Manual sync (without slash commands)
Restart opencode to run the startup sync flow (pull remote, apply if changed, push local changes if needed).
Inspect the local repo directly:
cd ~/.local/share/opencode/opencode-synced/repo
git status
git log --oneline -5If the sync repo has uncommitted changes, you can:
- Auto-resolve using AI: Run
/sync-resolveto let AI analyze and decide whether to commit or discard the changes - Manual resolution: Navigate to the repo and resolve manually:
cd ~/.local/share/opencode/opencode-synced/repo
git status
git pull --rebaseThen re-run /sync-pull or /sync-push.
How to completely remove and delete opencode-synced
Run this one-liner to remove the plugin from your config, delete local sync files, and delete the GitHub repository:
bun -e '
const fs = require("node:fs"), path = require("node:path"), os = require("node:os"), { spawnSync } = require("node:child_process");
const isWin = os.platform() === "win32", home = os.homedir();
const configDir = isWin ? path.join(process.env.APPDATA, "opencode") : path.join(home, ".config", "opencode");
const dataDir = isWin ? path.join(process.env.LOCALAPPDATA, "opencode") : path.join(home, ".local", "share", "opencode");
["opencode.json", "opencode.jsonc"].forEach(f => {
const p = path.join(configDir, f);
if (fs.existsSync(p)) {
const c = fs.readFileSync(p, "utf8"), u = c.replace(/"opencode-synced"\s*,?\s*/g, "").replace(/,\s*\]/g, "]");
if (c !== u) fs.writeFileSync(p, u);
}
});
const scp = path.join(configDir, "opencode-synced.jsonc");
if (fs.existsSync(scp)) {
try {
const c = JSON.parse(fs.readFileSync(scp, "utf8").replace(/\/\/.*/g, ""));
if (c.repo?.owner && c.repo?.name) {
const res = spawnSync("gh", ["repo", "delete", `${c.repo.owner}/${c.repo.name}`, "--yes"], { stdio: "inherit" });
if (res.status !== 0) console.log("\nNote: Repository delete failed. If it is a permission error, run: gh auth refresh -s delete_repo\n");
}
} catch (e) {}
}
[scp, path.join(configDir, "opencode-synced.overrides.jsonc"), path.join(dataDir, "sync-state.json"), path.join(dataDir, "opencode-synced")].forEach(p => {
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
});
console.log("opencode-synced removed.");
'- Remove
"opencode-synced"from thepluginarray in~/.config/opencode/opencode.json(or.jsonc). - Delete the local configuration and state:
rm ~/.config/opencode/opencode-synced.jsonc rm ~/.local/share/opencode/sync-state.json rm -rf ~/.local/share/opencode/opencode-synced
- (Optional) Delete the backup repository on GitHub via the web UI or
gh repo delete.
bun run buildbun run testbun run lint
To test the same artifact that would be published, install from a packed tarball into opencode's cache:
mise run local-pack-testThen set ~/.config/opencode/opencode.json to use:
{
"plugin": ["opencode-synced"]
}Restart opencode to pick up the cached install.
I stumbled upon opencodesync while publishing this plugin.
{ "$schema": "https://opencode.ai/config.json", "plugin": ["opencode-synced"], }