Skip to content

Repository files navigation

claude-backup

繁體中文說明請看 README.zh-TW.md

Automatic off-machine snapshots for Claude Code. Before an agent runs anything destructive — and at a few other checkpoints — your working tree is pushed to a separate backup repository, including changes you have not committed. If the project folder is deleted, the work still exists somewhere else.

Why this exists

An agent with write access will eventually delete the wrong thing. When that happens, the usual defences turn out to be thinner than they look:

  • Blocking delete commands covers one failure mode. Overwrites, truncation, git reset --hard, and a redirect into an existing file all still destroy work — and the list of command spellings never ends.
  • Claude Code's built-in file history snapshots a file before it is edited. A file that was deleted was never edited, so it was never snapshotted.
  • Committing often only helps if the repository survives. A forced delete of the project directory takes .git with it, and every local commit goes too.

The property that actually matters is that a copy exists outside the directory that can be deleted, and that creating it does not depend on the agent remembering to do it. That is what this plugin does: the snapshot is taken by a hook, before the destructive command runs, whether or not the model thought about it.

What a snapshot contains

Everything in the working tree at that moment, including staged changes, unstaged changes, and files git has never seen — anything not excluded by .gitignore and under the configured size limit.

It is stored as a normal git commit object pushed to backup/<project-name> in your backup repository. Each new snapshot is chained onto the previous one, so git log backup/<project-name> is a timeline you can walk back through.

What it deliberately does not touch

Your branch Never modified. No commits appear in your history, nothing triggers CI, teammates see nothing.
Your staging area The snapshot is built with a separate index file, so a partially staged git add is left exactly as it was.
Your working tree Read only. Nothing is checked out, reset, or cleaned.
Your origin Untouched. The backup uses its own remote, added automatically under a separate name.

When it runs

Event Trigger Purpose
SessionStart A conversation starts or resumes Records a clean "before the agent touched anything" state
PreToolUse A destructive shell command is about to run Snapshots first. If the push fails, the command is blocked.
Stop Each turn ends Rate limited (5 minutes by default) so you are not waiting on the network every turn
SessionEnd The conversation ends A final snapshot

Destructive commands are identified by the command name, parsed with shell tokenisation — not by searching the string. grep -rn rm src/ does not trigger a backup; cd build && sudo rm -rf . does. Detection covers rm, rmdir, unlink, shred, del, truncate, dd, mv, PowerShell's Remove-Item and Clear-Content, git reset --hard, git clean, git checkout ., git restore, and single-> redirects into a file.

Requirements

  • Claude Code
  • Python 3.8 or newer on PATH (as python3 or python)
  • git
  • A remote you can push to without an interactive prompt (SSH keys, or a credential helper)

Install

As a plugin

/plugin marketplace add loreias32/claude-backup
/plugin install claude-backup

Then create the config file described below. Until remote_url is set, the plugin does nothing at all — it will not block commands, warn, or interfere in any way.

To try it without installing:

git clone https://github.com/loreias32/claude-backup.git
claude --plugin-dir ./claude-backup

Manually

  1. Copy hooks/claude-backup.py and hooks/run.sh into ~/.claude/hooks/.
  2. Create the config file (below).
  3. Merge the hooks block from settings-snippet.json into ~/.claude/settings.json, replacing the placeholder with an absolute path. Merge it — do not overwrite an existing hooks object.
  4. Restart Claude Code.

Do not install both ways at once, or every event backs up twice.

Configuration

Create ~/.claude/backup-config.json:

{
  "remote_url": "git@github.com:you/claude-backups.git",
  "remote_name": "claude-backup",
  "throttle_minutes": 5,
  "block_on_push_failure": true,
  "max_file_mb": 50,
  "skip_paths": []
}
Field Default Meaning
remote_url "" Where snapshots go. Empty disables everything. Use SSH so pushes never prompt.
remote_name claude-backup The git remote name added to each project. Pick something that will not collide.
throttle_minutes 5 Minimum gap between Stop pushes. SessionStart, SessionEnd, and destructive-command backups ignore this.
block_on_push_failure true Whether a failed push blocks the destructive command that triggered it.
max_file_mb 50 Untracked files larger than this are left out, so model weights and datasets do not end up in the backup.
skip_paths [] Repositories at or under these paths are never backed up. Compared as resolved absolute paths, not substrings.

CLAUDE_BACKUP_CONFIG overrides the config file location if you need a different one per machine.

One backup repository or several

A single private repository can hold every project — each one lands on its own branch, and unrelated histories coexist without interfering. The trade-off is that one leak exposes everything. For work that must stay isolated, give it a dedicated backup repository and add it to skip_paths in the shared configuration.

Restoring

List the snapshots for a project:

git clone git@github.com:you/claude-backups.git
cd claude-backups
git log --oneline backup/my-project

Read a single file as it was at a point in time:

git show <commit>:src/thing.py

Check the whole tree out somewhere safe:

git worktree add /tmp/restore <commit>

Verifying it works

After installing and setting remote_url, start a session in a git repository and run:

git ls-remote <your-backup-remote> "refs/heads/backup/*"

A branch named after your project means the whole chain is working — hook, config, credentials, and remote.

Concurrent sessions

Two sessions open on the same project both push to the same branch. When the second push is rejected as non-fast-forward, the plugin fetches what is already there and creates a commit with both as parents rather than force-pushing over it. Neither session's snapshots are lost.

Limitations

Stated plainly, because a backup tool you trust incorrectly is worse than none:

  • Only git repositories are protected. The whole mechanism is built on git objects. A plain folder cannot be snapshotted.
  • A push requires the network. Offline, snapshots cannot leave the machine. With block_on_push_failure enabled, destructive commands are blocked while offline — that is the intended trade-off, and you can turn it off.
  • .gitignore is respected. Usually correct, but if your ignore rules exclude a file that matters, it is not backed up.
  • Detection is a list, and lists are incomplete. A destructive operation spelled in a way this plugin does not recognise — inside a script, through an interpreter, via an unusual tool — runs without a preceding snapshot. The scheduled snapshots limit how much that costs you, but they do not eliminate it.
  • It protects work, not secrets. Anything in your working tree that is not ignored, including local .env files, gets pushed. Keep the backup repository private, and use .gitignore and skip_paths deliberately.
  • The agent could disable it. An agent with write access to your configuration can turn off its own guardrails. This raises the cost of an accident; it is not a security boundary against deliberate action.

Tests

python tests/test_backup.py

Runs entirely in a temporary directory against a local bare repository. No network, and none of your repositories are touched. It covers command detection, snapshot contents, non-interference with your index and history, snapshot chaining, divergent remotes, blocking behaviour, and the disabled state.

License

MIT

About

Auto-snapshot your work to a separate repo before Claude Code does anything destructive. Survives a deleted project folder. 在 AI 動手前自動備份,連整個資料夾被刪掉都救得回來。

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages