Sync OpenCode data across machines using a private GitHub repository with vector clock conflict resolution
npm install @opencode-sync/opencode-syncSync your OpenCode configuration, sessions, and data across multiple machines using a private GitHub repository.
- Sync Everything - Config, sessions, messages, credentials, prompts, and more
- Multi-Machine Support - Safely sync across laptops, VMs, and servers simultaneously
- Conflict Resolution - Vector clocks detect conflicts, three-way merge resolves them
- Encrypted Credentials - AES-256-GCM encryption for sensitive data
- Auto-Sync - Syncs on startup, on file changes (2s debounce), and every minute
- Offline-Friendly - Works offline, syncs when reconnected
- Atomic Updates - Uses Git's compare-and-swap for safe concurrent access
Add to your opencode.json config file:
``json`
{
"plugin": ["opencode-sync"]
}
Clone into the global plugins directory:
`bash`
cd ~/.config/opencode/plugins
git clone https://github.com/ercin/opencode-sync
cd opencode-sync
npm install && npm run build
Or for project-specific installation:
`bash`
cd your-project/.opencode/plugins
git clone https://github.com/ercin/opencode-sync
cd opencode-sync
npm install && npm run build
| Category | Data | Default |
|----------|------|---------|
| Config | opencode.json, agents, commands, modes, tools, themes | ✅ Enabled |
| State | Model selections, prompt history, stashed prompts | ✅ Enabled |
| Credentials | OAuth tokens, MCP auth (encrypted) | ✅ Enabled |
| Sessions | Session metadata and history | ✅ Enabled |
| Messages | Conversation messages and parts | ❌ Disabled* |
| Projects | Project configurations | ✅ Enabled |
| Todos | Task lists and session diffs | ✅ Enabled |
*Messages sync is disabled by default because it can be very large (8MB+). Enable it in config if needed.
Each machine maintains a logical timestamp. When syncing:
1. Equal - Both in sync, no action needed
2. Local Ahead - Safe to push
3. Remote Ahead - Need to pull
4. Concurrent - Both changed, needs merge
When conflicts occur:
1. Find common ancestor (last synced version)
2. Compute diffs from ancestor to local and remote
3. If diffs don't overlap -> auto-merge
4. If diffs overlap -> apply conflict strategy
Configure in opencode-sync.json:
- auto-merge (default) - Attempt automatic mergelocal-wins
- - Keep local changes on conflictremote-wins
- - Keep remote changes on conflictnewest-wins
- - Keep whichever is newerask
- - Prompt for resolution
1. Create a GitHub Personal Access Token with repo scope
2. Set the environment variable:
`bash`
export GITHUB_TOKEN=ghp_your_token_here
3. Start OpenCode - the plugin will automatically create a private repo for sync storage
Add to your shell profile (~/.bashrc, ~/.zshrc) for persistence.
When the plugin starts with a valid token but no storage configured:
1. Detects your GitHub username automatically
2. Creates a new private repository called .opencode-sync.opencode-sync/
3. Initializes the directory with a manifest~/.config/opencode/opencode-sync.json
4. Saves the repo info to
5. Future runs will use the same repository
Instead of environment variable, create ~/.config/opencode/opencode-sync.json:
`json`
{
"token": "ghp_your_token_here"
}
Config file token takes precedence over environment variable.
After starting OpenCode, check the logs for:
``
[opencode-sync] Token loaded from: environment variable
[opencode-sync] Setting up sync storage...
[opencode-sync] Creating sync repository...
[opencode-sync] Linked to repo: username/.opencode-sync
[opencode-sync] Repo saved to config
[opencode-sync] Plugin ready
On subsequent runs:
``
[opencode-sync] Token loaded from: environment variable
[opencode-sync] Linked to repo: username/.opencode-sync
[opencode-sync] Plugin ready
`json`
{
"token": "ghp_your_token_here",
"repoOwner": "auto-detected-username",
"repoName": ".opencode-sync",
"autoSyncOnStartup": true,
"continuousSync": true,
"syncIntervalMinutes": 1,
"sync": {
"config": true,
"state": true,
"credentials": true,
"sessions": true,
"messages": false,
"projects": true,
"todos": true
},
"conflictStrategy": "auto-merge"
}
Note: messages is false by default because conversation history can be very large (8MB+). Set to true if you want to sync messages across machines.
The plugin syncs at these times:
| Trigger | When |
|---------|------|
| Startup | Immediately when OpenCode starts |
| File Changes | 2 seconds after local OpenCode files change |
| Interval | Every 1 minute (configurable) |
This is not realtime - there's typically a 1-60 second delay depending on when changes occur.
- Credentials are encrypted with AES-256-GCM before upload
- Encryption key is derived from your passphrase using PBKDF2
- The repository is created as private (not public)
- Token is stored locally, never uploaded
- Atomic commits with compare-and-swap prevent race conditions
``
src/
├── index.ts # Main entry point (re-exports from plugin)
├── plugin/ # Plugin module
│ ├── plugin.ts # Plugin definition and hooks
│ ├── state-manager.ts # Plugin state management
│ ├── sync-handler.ts # Sync operation handler
│ └── types.ts # Plugin types
├── storage/ # Storage backend abstraction
│ ├── interface.ts # StorageBackend interface
│ ├── repo/ # GitHub Repo backend
│ │ ├── repo-client.ts # API client
│ │ ├── fetch.ts # Fetch with retry logic
│ │ └── errors.ts # API error types
│ └── index.ts # Exports
├── sync/
│ ├── engine/ # Sync engine module
│ │ ├── sync-engine.ts # Core orchestration
│ │ ├── state.ts # Local state management
│ │ ├── manifest.ts # Manifest operations
│ │ ├── result.ts # Result builders
│ │ └── errors.ts # Sync error types
│ ├── operations/ # Push/pull/merge operations
│ │ ├── push.ts # Push to remote
│ │ ├── pull.ts # Pull from remote
│ │ ├── merge-operation.ts # Merge conflicts
│ │ └── helpers.ts # Encryption helpers
│ ├── merge/ # Three-way merge module
│ │ ├── json-merge.ts # JSON merge algorithm
│ │ ├── jsonl-merge.ts # JSONL merge algorithm
│ │ └── utils.ts # Merge utilities
│ ├── watcher/ # File watcher module
│ │ ├── file-watcher.ts # Main watcher class
│ │ ├── directory-watcher.ts # Directory watching
│ │ └── ignore-patterns.ts # File ignore rules
│ ├── vector-clock.ts # Vector clock operations
│ └── packer.ts # Compression/chunking
├── crypto/
│ └── encrypt.ts # AES-256-GCM encryption
├── data/ # Data loading module
│ ├── category-loader.ts # Load by category
│ ├── directory-loader.ts # Directory traversal
│ ├── parsers.ts # JSON/JSONL parsing
│ ├── writer.ts # Write local data
│ └── state.ts # Config/state persistence
└── types/ # TypeScript definitions
├── config.ts # Config types
├── manifest.ts # Manifest types
├── sync.ts # Sync result types
├── vector-clock.ts # Vector clock types
└── paths.ts # Path configuration
`bashInstall dependencies
npm install
Code Quality
This project uses strict linting rules optimized for LLM readability and maintainability:
$3
- Max 200 lines per file (excluding blanks/comments)
- Max 60 lines per function
- Max 4 levels of nesting
- Max 5 parameters per function
- Max 20 statements per function
- Cyclomatic complexity limit of 15$3
- Explicit return types required
- Explicit member accessibility required
- No any` types allowedMIT