Automatic code snapshots and optional offsite backups for opencode workflows.
With agentic coding, changes pile up faster than you can commit them. You can't review and commit after every prompt, and you shouldn't have to. opencode-snapshot plugin gives you automatic undo, revert to any previous state without manual commits or fear of losing work.
On every prompt you submit, the plugin:
- Local — records a full-tree git snapshot into a dedicated ref
(default
refs/opencode/snapshots). Never touchesHEAD, your branches, your index, or the working tree. Skips silently outside git repos. - External — pushes source +
.gitto external repositories on a target path (any mounted location), throttled (default: max one push per 2 min) and serialized so rapid prompts never stack restic processes. Repos are auto-created on first use.
YOU SUBMIT A PROMPT
│
▼
┌──────────────────────┐
│ chat.message hook │
│ (fires each prompt) │
└──────────────────────┘
│
┌────┴────┐
▼ ▼
┌──────────────┐ ┌──────────────────────┐
│ LOCAL │ │ EXTERNAL │
│ snapshot │ │ backup │
│ (sync/await) │ │ (async, throttled) │
└──────────────┘ └──────────────────────┘
│ │
▼ ▼
┌────────────────────────────────────────┐
│ VERBOSE LOG (.opencode/snapshot.log) │
│ + on failure: Desktop error + toast │
└────────────────────────────────────────┘
Copy index.ts into your global plugins directory, then restart opencode:
mkdir -p ~/.config/opencode/plugins
cp index.ts ~/.config/opencode/plugins/opencode-snapshot.tsTo enable it for a single project instead, copy it to that project's
.opencode/plugins/opencode-snapshot.ts.
git log --oneline refs/opencode/snapshots
git checkout <sha> -- path/to/file # restore a file
git checkout <sha> -- . # restore everythingSnapshots live in each repo's own .git, so git push origin main never
publishes them — they stay on your machine.
restic -r <externalBackup>/<project>/src --insecure-no-password snapshots
restic -r <externalBackup>/<project>/src --insecure-no-password restore latest --target /tmp/out
# default externalBackup is /mnt/pendrive/opencode-backup; replace with your pathEvery prompt becomes one snapshot in the local chain, so you can walk, diff, and restore any point-in-time state:
git log --oneline refs/opencode/snapshots # per prompt: one commit each
git diff <sha>~1 <sha> # what changed at a given prompt
git log -p --all -- path/to/file # file's full history across snapshots
git show <sha>:path/to/file # a file as it was at a prompt
git checkout <sha> -- path/to/file # restore one file
git checkout <sha> -- . # restore everything to that prompt- Each snapshot commit records the full working tree including untracked
files (only
excluded paths and.gitignored files are left out). - Snapshots are per-project, isolated in that repo's
.git, and never pushed bygit push origin main. - Because each snapshot is a full tree, you can always restore exactly what the code looked like before a given prompt — no partial/broken state.
- Offsite copies live in the restic repos (
src/git); restore the latest of either via therestic restore ... --targetcommands above.
Global config at ~/.config/opencode/snapshot.json — every key is optional and has a sensible default. Only include what you want to change. Per-project overrides in <project>/.opencode/snapshot.json.
All keys are optional. The defaults below are what you get with zero config —
localSnapshotson,externalBackupoff, restic throttled every 2 minutes, 50 source / 20 git snapshots retained, prompts and verbose log enabled. Only add a key when you want something different.
Omit a key entirely to use its default.
Replace any step with a different tool by overriding its command string. Placeholders: {repo} (quoted target path), {what} (source: . or .git), {excludes} (only in default backupCommand), {keep} (retention count). Set a command to "none" to skip it.
{
// borg example — same shape, different tool
"checkCommand": "borg info {repo}",
"initCommand": "borg init --encryption=none {repo}",
"backupCommand": "borg create {repo}::opencode-{now} {what}",
"retentionCommand": "borg prune --keep-last {keep} {repo}"
// rsync example — needs no init/check/retention
// "checkCommand": "none",
// "initCommand": "none",
// "backupCommand": "rsync -a --delete {what}/ {repo}/",
// "retentionCommand": "none"
}git(for local snapshots)- restic by default when
externalBackupis set; swap via command strings above for other tools - Target path must be a mounted filesystem; missing mounts are skipped with a warning
MIT
{ // ── Local snapshots ────────────────────────────────────────────── "localSnapshots": true, // false = disable entirely "refName": "refs/opencode/snapshots", // git ref for the chain "skipIfUnchanged": true, // skip when tree == last snapshot // ── External backups ───────────────────────────────────────────── // Needs restic installed and target path mounted; omit all to stay local-only. "externalBackup": "/mnt/pendrive/opencode-backup", // target path; omit = disabled "minIntervalSec": 120, // max pushes per 2 min; 0 = every prompt "exclude": ["node_modules", ".cache", "dist", "target"], // ── Layout ─────────────────────────────────────────────────────── "separateRepos": true, // true: <root>/<project>/{src,git} // false: <root>/<project>/all "backupGit": true, // false = skip .git (separateRepos only) // ── Retention ──────────────────────────────────────────────────── // Old snapshots pruned automatically. Only the keys matching your layout apply. "keepLast": 50, // mixed repo (separateRepos: false) "keepLastSource": 50, // src repo (separateRepos: true) "keepLastGit": 20, // git repo (separateRepos: true) // ── Logging ────────────────────────────────────────────────────── // Error log: one file per failure on Desktop. Verbose log: always in project and included in restic src backup. // [ok local] <iso> | opencode pre-prompt | <sha> | <prompt> (prompt pipe-escaped to ¦, empty → trailing |) // [ok src|git] (or src fail|git fail + [error] detail lines + Desktop file + toast on failure) // Path variables: {project}, {timestamp}, {date}, {hour}, ~/ "logErrorPath": "~/Desktop/snapshot-{project}-ERROR-{timestamp}.txt", "logVerbosePath": "<project>/.opencode/snapshot.log", "logIncludePrompt": true, // include prompt text (no token cost) "notifyOnError": true, // TUI toast on failure // ── Backend commands (restic by default) ───────────────────────── // All four use {repo}/{what}/{excludes}/{keep} placeholders where applicable. // Omit a key for the restic default; set to "none" to disable that step. // exclude[] only feeds the default backupCommand — custom commands own their excludes. // "initCommand": "restic -r {repo} --insecure-no-password init", // "checkCommand": "restic -r {repo} --insecure-no-password cat config", // "backupCommand": "restic -r {repo} --insecure-no-password backup {what} --tag opencode-pre-prompt {excludes}", // "retentionCommand": "restic -r {repo} --insecure-no-password forget --keep-last {keep} --prune" }