Automated and manual backup system based on Borg Backup with a terminal interface (TUI) and native systemd timers.
100% Bash. Zero runtime dependencies beyond the standard GNU/Linux ecosystem.
Developer: morphilab
- Manual and automatic backups — local or remote via SSH
- Client-side encryption with Borg (
repokey) - Deduplication — only changes between backups are stored
- Configurable retention per job (daily, weekly, monthly)
- TUI interface with
whiptailfor browsing and extracting backups - Native automation with
systemd --user timers(no custom daemon) - Detailed JSON logs with timestamps, exit codes, and executed commands
- No credentials in code — uses the system's
~/.ssh/config - Secrets never touch disk —
BORG_PASSPHRASEis never persisted; timers only receiveBORG_PASSCOMMAND - File locking (atomic) — prevents concurrent executions of the same job
- Signal trapping — automatic cleanup on Ctrl+C / SIGTERM / SIGHUP
- Config validation — enforced fail-fast at load time (anti-injection, whitelists, required fields)
- Dry-run mode —
./copycrow.sh dryrun <job>simulates without writing - Repository verification —
verify <job>/verify-allrunborg check; optionalverify_scheduletimer detects silent corruption - Failure notifications — optional
[global] on_failure_cmdhook fired when a backup or verification fails - Health check —
doctorpreflights borg (local/remote), SSH, passphrase strategy, linger, systemd session, disk space and directory permissions - Log retention —
[global] logs_retention_daysauto-purges old JSON logs (default 30) - Bash completion — commands, job names and hosts (
completions/copycrow.bash)
This project was developed with assistance from artificial intelligence tools. Given the automated nature of some components, users are advised to review and test the code independently before integrating it into their own systems.
| Dependency | Installation |
|---|---|
borgbackup |
sudo apt install borgbackup |
whiptail |
sudo apt install whiptail |
bash 4+ |
Included in Ubuntu/Debian |
systemd |
Included in Ubuntu/Debian |
pass (recommended) |
sudo apt install pass |
git clone https://github.com/morphilab/copycrow.git
cd copycrow
./copycrow.sh initThis creates copycrow.conf (your configuration) from the example.
Create a dedicated key and configure two aliases:
ssh-keygen -t ed25519 -f ~/.ssh/copycrow_server -N "" -C "copycrow"
ssh-copy-id -i ~/.ssh/copycrow_server.pub user@1xx.1xx.1.1xxEdit ~/.ssh/config:
Host nas-backup
HostName 1xx.1xx.1.1xx
User backupuser
IdentityFile ~/.ssh/id_ed25519 # your daily key
Host nas-backup-borg
HostName 1xx.1xx.1.1xx
User backupuser
IdentityFile ~/.ssh/copycrow_server # dedicated key without passphrase
In copycrow.conf use host = nas-backup-borg. See the Security section.
sudo apt install pass
gpg --gen-key # generate GPG key
pass init "your-gpg-id" # initialize pass
pass insert copycrow/borg # store passphrase
export BORG_PASSCOMMAND="pass show copycrow/borg"Add to ~/.bashrc for persistence.
[global]
retention_default = --keep-daily 7 --keep-weekly 4 --keep-monthly 6
compression = lz4
mount_dir = .mnt
logs_dir = logs
# timeout_start_sec = infinity # optional; default 3600 (1 h)
[daily_job]
type = automatic
sources = /home /etc
host = nas-backup-borg
remote_path = /backups/copycrow/daily
schedule = daily
retention = --keep-daily 7 --keep-weekly 4Create a key without a passphrase for automation while keeping your daily key with a passphrase. Use two SSH aliases to the same server:
ssh-keygen -t ed25519 -f ~/.ssh/copycrow_server -N "" -C "copycrow"
ssh-copy-id -i ~/.ssh/copycrow_server.pub user@serverIn ~/.ssh/config:
Host my-server
HostName 1xx.1xx.1.1xx
User user
IdentityFile ~/.ssh/id_ed25519 # daily key with passphrase
Host my-server-borg
HostName 1xx.1xx.1.1xx
User user
IdentityFile ~/.ssh/copycrow_server # dedicated key without passphrase
In copycrow.conf use host = my-server-borg.
In the server's ~/.ssh/authorized_keys, restrict the key to only run borg:
command="borg serve --restrict-to-path /path/backups",no-port-forwarding,no-pty ssh-ed25519 AAA...
pass encrypts the passphrase with GPG. Avoids plaintext in ~/.bashrc:
sudo apt install pass
gpg --gen-key # generate GPG key
pass init "your-gpg-id" # initialize
pass insert copycrow/borg # store passphrase
export BORG_PASSCOMMAND="pass show copycrow/borg"Add to ~/.bashrc for persistence. Copycrow propagates BORG_PASSCOMMAND to timers automatically.
Note: copycrow never writes your passphrase to disk. If only
BORG_PASSPHRASEis set, automatic timers will refuse to persist it and backups will fail until you configureBORG_PASSCOMMAND. Running./copycrow.sh uninstallremoves the generated environment file.
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519./copycrow.sh # Open interactive menu (TUI)
./copycrow.sh init # Initial setup
./copycrow.sh backup <job> # Manual backup of a job
./copycrow.sh manual <job> # Alias of backup
./copycrow.sh auto <job> # Automatic backup (used by timers)
./copycrow.sh dryrun <job> # Simulate backup (writes nothing)
./copycrow.sh list [job] # List backups
./copycrow.sh open <host> <arch> # Extract and open container
./copycrow.sh verify <job> # Repository integrity check (borg check)
./copycrow.sh verify-all # Verify every configured repository
./copycrow.sh sync <job> # Replicate a local repo to Proton Drive
./copycrow.sh doctor # One-shot system health check
./copycrow.sh migrate # Convert legacy config to English v1.0.0
./copycrow.sh install # Install systemd timers
./copycrow.sh uninstall # Remove timers
./copycrow.sh status # System status
./copycrow.sh --version # Print version
./copycrow.sh help # HelpVerify repositories (borg check):
./copycrow.sh verify daily_job # one-off integrity checkAdd verify_schedule = monthly to [global] and run install to get a
copycrow-verify.timer that checks every configured repository on a schedule.
Get notified when a backup fails:
[global]
on_failure_cmd = notify-send "copycrow FAILED"The hook runs WITHOUT a shell (charset-validated at load; no injection surface)
and receives context via environment variables:
COPYCROW_FAILED_JOB, COPYCROW_FAILURE_ARCHIVE, COPYCROW_FAILURE_EXIT_CODE.
A failing hook is logged but never alters the backup's own result.
Keep logs under control: logs_retention_days = 30 in [global] purges
logs/copycrow-*.log older than N days automatically after each backup.
CopyCrow can replicate any local repository (host = local) to
Proton Drive immediately after each successful
backup, using Proton's official
proton-drive CLI. Your data gets
two independent encryption layers: Borg client-side plus Drive E2E.
# Keyring + D-Bus session support (needed by the CLI)
sudo apt install libsecret-1-0 dbus-x11
# Download the Linux binary from https://proton.me/download/drive/cli
chmod +x proton-drive && sudo mv proton-drive /usr/local/bin/
# Sign in once (browser opens; session is stored in your OS keyring)
dbus-run-session -- proton-drive auth login[global]
# Optional, only if the binary is not on PATH:
# cloud_cli_path = /usr/local/bin/proton-drive
[local_job]
host = local
remote_path = /mnt/external-backup/copycrow
# The PRESENCE of this key enables offsite replication for the job:
cloud_remote = /Backups/local_jobEach backup of that job now ends with an incremental upload: a local
manifest (~/.cache/copycrow/cloud/<job>.manifest, size+mtime) detects
unchanged files, so only new/rewritten repository segments travel.
Manual retry and inspection:
./copycrow.sh sync <job> # retry/force replication
./copycrow.sh doctor # validates binary, D-Bus wrapper and session- Headless machines and systemd timers are supported: every CLI call is
automatically wrapped in
dbus-run-session. Keeplibsecret-1-0,dbus-x11installed and the login keyring unlocked. - The first sync uploads the whole repository (~1 API request per file);
consider
timeout_start_sec = infinityfor large repos. - Repository files removed by Borg pruning are NOT deleted remotely yet (the official CLI has no reliable delete); remote usage grows slowly over time.
- Restoring elsewhere: download the
cloud_remotefolder with any Proton app, then point borg at the downloaded folder as a normal repository.
# Ad hoc:
source completions/copycrow.bash
# Persistent:
mkdir -p ~/.local/share/bash-completion/completions
cp completions/copycrow.bash ~/.local/share/bash-completion/completions/copycrowCompletes commands, job names (from the active conf) and SSH hosts.
copycrow/
├── copycrow.sh ← entry point
├── copycrow.conf ← your config (gitignored)
├── copycrow.conf.example ← example configuration
├── README.md
├── LICENSE
├── CHANGELOG.md
├── SECURITY.md
├── VERSION
├── .gitignore
├── .shellcheckrc
├── src/
│ ├── config-parser.sh ← INI parser + validation
│ ├── safety.sh ← traps, locks, cleanup
│ ├── backup-core.sh ← Borg wrapper + JSON logs
│ ├── timer-generator.sh ← systemd timers
│ ├── doctor.sh ← health check
│ ├── tui.sh ← whiptail menus
│ └── cloud-sync.sh ← Proton Drive offsite replication
├── completions/
│ └── copycrow.bash ← bash completion
├── tests/ ← bats-core tests
│ ├── backup-core.bats
│ ├── cli.bats
│ ├── completions.bats
│ ├── config-parser.bats
│ ├── doctor.bats
│ ├── safety.bats
│ ├── timer-generator.bats
│ ├── tui.bats
│ └── integration-extract.bats
├── .mnt/ ← temporary extraction
├── .locks/ ← mutual exclusion locks
└── logs/ ← daily JSON logs
# Requires: bats-core (https://github.com/bats-core/bats-core)
sudo apt install bats
bats tests/shellcheck -x copycrow.sh src/*.sh completions/copycrow.bash./copycrow.sh installThis creates systemd --user timers for all jobs with type=automatic.
Important: User timers only run when a login session is active. For always-on execution (even without login):
loginctl enable-linger"Permission denied (publickey)"
- Verify
~/.ssh/confighas the correctIdentityFilefor that host - If using a key with a passphrase, unlock it with
ssh-add - Test the key on the server:
ssh user@host "echo ok"
"Is borg working on the server?" Borg is not installed on the remote server:
ssh host "sudo apt install borgbackup""missing_passphrase" (in automatic timers)
You need BORG_PASSCOMMAND configured — BORG_PASSPHRASE alone is never persisted to disk (by design). See the Security section.
"BORG_PASSPHRASE is not set" / passphrase prompts in timers
Timers cannot answer interactive prompts. Configure the passphrase via pass + BORG_PASSCOMMAND (see Security).
"Repository does not exist" The repo is initialized automatically on the first backup. If it fails, check:
- SSH connection to the host:
ssh host "echo ok" - Borg installed on the remote server
- Write permissions on
remote_path
"copycrow.conf not found"
./copycrow.sh initTimers not running
systemctl --user list-timers 'copycrow-*'
loginctl enable-lingerSSH key with passphrase fails in timers
Timers run outside your login session: there is no ssh-agent and the ephemeral
SSH_AUTH_SOCK is not persisted (it goes stale after a reboot). Options:
- Use a dedicated passphrase-less key for backups (see Security), or
- Expose an agent to the user systemd session (a user-level
ssh-agent.service, plusAddKeysToAgent yesin~/.ssh/config).
"Unknown config keys after upgrading?" If you upgraded from an older version with Spanish config keys:
./copycrow.sh migrate # Converts config to English v1.0.0 formatA backup is saved as copycrow.conf.bak.
MIT — See LICENSE file.