简体中文 · Quick start · Overrides · Sessions · Commands
You use OpenCode on more than one machine. Your config, agents, commands, skills and MCP servers live on whichever one you touched last.
opencode-github-sync keeps them all in step through a private GitHub repository — as an OpenCode plugin, as a CLI, or both.
npm install -g opencode-github-sync
opencode-sync init # creates the private repo and configures this machine
opencode-sync pushMost config-sync tools stop being safe the moment real use hits them. These are the three places that happens, and what this one does instead.
| Typical approach | Here | |
|---|---|---|
| Machine differences | Every machine forced identical | Per-machine overrides that sync can never overwrite |
| Sessions | Commit opencode.db — gigabytes, unmergeable |
Selective per-session shards, a few MB, conflict-free |
| When it breaks | Plugin-only: bad config means no way back | The CLI runs outside OpenCode and can always recover you |
Plus the boring things that turn out to matter:
- Symlinks are refused, not followed. No copying files out of the machine by accident, no junction turning a delete into the wrong delete.
- Every replacement is atomic. A crash mid-sync cannot leave a half-written config that stops OpenCode from starting.
- A push is verified against the remote.
git pushcan exit0without the remote moving; that is checked, not assumed. - Nothing runs twice at once. A cross-process lock covers the plugin, the CLI and every OpenCode window.
- Credentials are opt-in and refused outright on a public repository.
- Your hostname never leaves the machine. Commits are signed with a stable pseudonym, so a corporate asset tag never lands in a repo that might not stay private.
OpenCode installs it on the next start. The plugin adds a pull on startup, an
optional push when a session goes idle, and an opencode_sync tool so you can
just ask for a sync in plain language.
npm install -g opencode-github-syncKeep the CLI even if you use the plugin. A plugin cannot fix the configuration that stops OpenCode from loading the plugin.
Requirements — git, and gh for automatic setup.
Node 18+ for config sync; Node 22.5+ or Bun for session sync.
opencode-sync init # → <you>/my-opencode-config
opencode-sync init team-config # custom name
opencode-sync init my-org/shared # inside an organisation
opencode-sync pushinit creates a private repository, writes
~/.config/opencode/opencode-sync.jsonc, and leaves an empty overrides file
ready for you.
opencode-sync link doomsday616/my-opencode-config
opencode-sync pullThen restart OpenCode.
opencode-sync push # share what changed here
opencode-sync pull # take what changed elsewhere
opencode-sync status # what is in sync, what is not| Source | In the repo | Contents |
|---|---|---|
~/.config/opencode/ |
repo root | config, agents, commands, skills, themes, MCP |
~/.local/share/opencode/ |
_data/ |
project metadata, optional credentials |
~/.local/state/opencode/ |
_state/ |
frecency, model cache, prompt history |
~/.agents/skills/ |
_agents/ |
skills installed by the skills CLI |
| selected sessions | _sessions/ |
one gzipped shard per session |
Never synced: opencode.db and its journals, tool output, snapshots, logs,
and your local settings and overrides files.
Add anything else with extraPaths:
{
"extraPaths": [".tavily", ".config/gh/config.yml"]
}A synced repository makes every machine identical. That is exactly wrong for the settings that are genuinely machine-specific: a corporate proxy, a local toolchain path, an MCP server that only exists on one box.
Put those in ~/.config/opencode/opencode-sync.overrides.jsonc:
{
"model": "github-copilot/claude-sonnet-4",
"mcp": {
"playwright": { "enabled": true }
}
} repo opencode.jsonc shared baseline, committed
+ overrides.jsonc this machine only, never committed
───────────────────────── deep merge
= effective opencode.jsonc what OpenCode actually reads
Merge rules: objects merge key by key, arrays and scalars replace, null
deletes a key.
The half that other tools get wrong is push. Because the effective file is what sits on disk, a naive push uploads your overrides to everyone. Here, any key the overrides file claims is restored to the value the repository already had before committing — so an overridden key is invisible to sync in both directions, and your machine keeps its own settings the whole time.
With no overrides file the config is copied byte for byte and your comments and formatting are untouched. The structural rewrite only happens once you actually opt in.
Off by default. Turn it on only for a private repository — conversations are private data.
opencode-sync sessions enable
opencode-sync sessions list
opencode-sync sessions include ses_065cad1caffeN3lf1RgLZno30 # pin one forever
opencode-sync sessions exclude ses_0ba19b4e7ffeMbSmWYi3Nj6xfZ # never sync this oneA real OpenCode database reaches several gigabytes, almost all of it tool output
inside part.data. Git cannot delta-compress it, GitHub's LFS free tier is one
gigabyte, and copying a live database alongside its write-ahead log can capture
a torn state that only shows up much later.
So sessions are exported one at a time. Each becomes an independent gzipped JSON shard holding its own rows plus the project and workspace rows it needs:
- No large files. Shards are small and compress well.
- Conflicts isolate themselves. Two machines working on different sessions
touch different files, so git merges them with no special handling. The only
real conflict is one session edited in two places, and there the newer
time_updatedwins — an in-progress conversation is never clobbered by a stale copy. - You choose what travels. A time window, an explicit include list, a project filter and a per-session size cap. Ancient sessions stay where they are.
Import is an upsert inside a transaction, so a failure leaves your database exactly as it was.
{
"sessions": {
"enabled": true,
"days": 7, // only sessions touched this recently
"maxSessions": 50, // hard cap per push, newest first
"maxSessionBytes": 5242880, // skip any single runaway session
"include": [], // always sync these, whatever the window says
"exclude": [], // never sync these
"directories": [] // only sessions under these project directories
}
}The internal
eventlog is deliberately excluded. It is the largest table by row count and nothing about resuming a conversation depends on it.
| Command | What it does |
|---|---|
opencode-sync init [name] |
Create a private sync repository |
opencode-sync link <owner/repo> |
Point this machine at an existing one |
opencode-sync push |
Share this machine's configuration |
opencode-sync pull |
Apply the shared configuration |
opencode-sync status |
Show what is in sync and what is not |
opencode-sync sessions list |
Recent sessions and their ids |
opencode-sync sessions enable |
Turn selective session sync on |
opencode-sync sessions include |
Pin a session, ignoring the time window |
opencode-sync overrides |
Create/locate the per-machine patch |
opencode-sync config |
Print the current settings |
| Flag | Effect |
|---|---|
--force |
Overwrite the other side on conflict (confirmed) |
--dry-run |
Report what would change, write nothing |
| Environment variable | Effect |
|---|---|
OPENCODE_SYNC_HOST_ALIAS |
Name this machine in commit messages |
OPENCODE_SYNC_VERBOSE=1 |
Full file list and stack traces |
NO_COLOR |
Disable colour |
SYNC_REMOTE_URL |
Override the repository URL |
~/.config/opencode/opencode-sync.jsonc — never committed.
{
"repo": { "owner": "you", "name": "my-opencode-config", "branch": "main" },
"machineAlias": "laptop", // optional; otherwise a stable pseudonym
"includeCredentials": false, // auth.json / account.json — private repos only
"includeSkills": true,
"includeState": true,
"extraPaths": [],
"sessions": { "enabled": false },
"autoPullOnStartup": true, // plugin only
"autoPushOnIdle": false // plugin only
}A pull conflicted with local edits. The worktree is rolled back to a clean state and your changes stay in the stash. The exact recovery commands are printed. Nothing is lost.
cd ~/.config/opencode
git stash list
git stash show -p stash@{0}"This machine has N commits that were never pushed." Deliberate — pulling
would destroy them. Run opencode-sync push, or pull --force to discard.
Authentication failed. gh auth login -h github.com. The token needs the
repo scope for private repositories.
Session sync says no SQLite driver. Node 22.5+ or Bun. Config sync is unaffected.
Everything is broken and OpenCode will not start. This is why the CLI exists:
opencode-sync pull --forcenpm install
npm run check # lint + typecheck + tests
npm run buildThe test suite runs against a real bare git repository on disk and simulates two separate machines, so push, pull, force, rebase, stashing and the override round-trip are all exercised end to end.
If this saved you some time, a ⭐ is appreciated.