< RETURN TO /PROJECTS

Baki

7/15/2026|AUTHOR: vexdynamics|
#go#open-source

rsync backup TUI, scheduler, CLI, and MCP server with point-in-time snapshots, tiered SquashFS retention, and WireGuard auto-join

Language: Go · View on GitHub ↗


Baki TUI

Baki is a terminal-based frontend for managing and executing rsync backup jobs over SSH. It provides both an interactive Text User Interface (TUI) and a background service mode for scheduled and triggered backups.

Features

  • Point-in-time snapshots: every run is a dated snapshot; unchanged files are hard-linked against the previous one, so history costs only the delta. A latest symlink always points at the newest good backup.
  • Tiered (GFS) retention: keep N daily snapshots; aged-out ones become weekly/monthly SquashFS archives with gzipped manifests — compressed, mountable, and still instantly listable/searchable.
  • Restore anywhere: whole snapshot, one file, or a directory — from any snapshot or archive — to a local directory or pushed back to the source server over SSH.
  • Search: find files by name across all snapshots and archive manifests of a job, with size and mtime.
  • TUI: job management, live status with server up/down indicator, snapshot browser, in-backup file explorer, filename search, and mc-style confirmation/progress/result dialogs for every restore.
  • CLI: everything is also scriptable — add, edit, remove, list, run, status, snapshots, search, restore, setup-host, service-install — with --json output for automation and AI agents.
  • Server mode: cron-style scheduler (baki server) that watches backup_jobs.yaml for changes, picks up manual triggers, prevents overlapping runs, and writes a heartbeat the TUI/CLI report on.
  • One-command deployment: sudo baki service-install writes, enables, and starts a systemd unit; baki setup-host generates SSH keys and prints exactly what to run on a new source machine.

Prerequisites

  • Go (for building the application)
  • rsync installed on the machine running Baki.
  • ssh client installed on the machine running Baki.
  • squashfs-tools (optional) — only needed for weekly/monthly archive tiers and restoring from them.
  • SSH access configured between the machine running Baki and the remote hosts defined in the jobs (key-based authentication is expected).

Installation

Prebuilt binaries

Tagged releases ship prebuilt binaries on the releases page for:

FileTarget
baki-linux-arm64Raspberry Pi 3B/4/5 with a 64-bit OS, other ARM64 Linux
baki-linux-armv7Raspberry Pi 2/3 with 32-bit Raspberry Pi OS (armhf)
baki-linux-armv6Raspberry Pi 1 / Zero / Zero W
baki-linux-amd64x86-64 Linux
baki-darwin-arm64 / baki-darwin-amd64macOS (Apple Silicon / Intel)
bash
curl -LO https://github.com/vexdynamics/baki/releases/latest/download/baki-linux-arm64
chmod +x baki-linux-arm64 && sudo mv baki-linux-arm64 /usr/local/bin/baki

Not sure which one your Pi needs? uname -maarch64 → arm64, armv7l → armv7, armv6l → armv6.

Build from source

  1. Clone the repository (or ensure you have the source code):

    bash
    # git clone <your-repo-url>
    # cd <repository-directory>
    
  2. Build the binary:

    bash
    go build -o baki .
    

    This will create an executable file named baki in the current directory.

Usage

Baki can be run in two modes: TUI (default) or Service.

TUI Mode

Run the application directly:

bash
./baki

This will launch the interactive terminal interface.

TUI Controls:

  • ↑/↓: Navigate the list of backup jobs.
  • a / n: Add a new backup job (opens the form).
  • e: Edit the selected backup job (opens the form).
  • d: Delete the selected backup job (requires confirmation implicitly via saving).
  • r: Manually trigger a run for the selected job. This creates a trigger file that the service mode (if running) will detect and execute.
  • s: Browse the selected job's snapshots and archives. Enter opens a file explorer inside the highlighted snapshot/archive (archives are browsed via their manifest, no decompression); r/R restores the highlighted snapshot — or, inside the explorer, the highlighted file/directory — locally / to the source server.
  • /: Search the selected job's backups by filename (all snapshots + archive manifests); Enter on a hit restores that single file to /tmp/baki/.
  • q / Ctrl+C: Quit the application.

Form Controls (Add/Edit View):

  • Tab / ↓ / Enter: Move to the next input field.
  • Shift+Tab / ↑: Move to the previous input field.
  • Enter (on last field): Save the job (validates required fields and schedule format).
  • Esc: Cancel and return to the job list view.

CLI Mode

All job management is also available non-interactively, for scripting or driving Baki from another program:

bash
# Prepare SSH access to a new source host: creates ~/.ssh/baki (ed25519) if
# missing and prints the exact command to authorize it on the remote machine;
# --check verifies key-based access works.
./baki setup-host --host HOST --user USER [--port 22] [--key ~/.ssh/baki] [--check]

# Add a job (required: --name --user --host --source --dest)
./baki add --name "My Server Home" --user backupuser --host server.example.com \
    --source /home/user/data --dest /mnt/backups/home \
    --schedule "0 6 * * *" --retention 14 [--port 22] [--key ~/.ssh/id_baki]

# Inspect jobs (--json for machine-readable output)
./baki list [--json]
./baki status [JOB] [--json]

# Change only the given flags; JOB is a job id or name
./baki edit JOB --schedule "0 3 * * *" --retention 30

# Trigger a run now (the running server picks it up within 10s)
./baki run JOB

# Remove a job (snapshots on disk are kept)
./baki remove JOB

# List restorable points in time for a job (daily snapshots + weekly/monthly archives)
./baki snapshots JOB [--json]

# Find a file across every snapshot and archive (case-insensitive substring)
./baki search JOB report.txt [--json]

# Restore — purely local, works without the server running
./baki restore JOB                                  # latest snapshot -> /tmp/baki/<job>_<snapshot>/
./baki restore JOB 2026-07-10_06-00-01              # a specific point in time
./baki restore JOB --file docs/report.txt           # a single file/dir from the latest snapshot
./baki restore JOB 2026-07-10_06-00-01 \
    --file docs/report.txt --to /home/user/recovered  # specific file, specific time, custom target

# Push the restore back to the job's SOURCE SERVER instead (lands in /tmp/baki/ there);
# uses the job's SSH credentials. --to overrides the remote path.
./baki restore JOB --remote
./baki restore JOB weekly_2026-W27 --file docs/report.txt --remote

Commands that write the config print a confirmation with the job id; the running server reloads the config automatically. status --json includes server_up so a caller can tell whether the scheduler is actually running.

Using Baki from AI Agents (MCP)

baki mcp serves all operations as Model Context Protocol tools over stdio, so any MCP client (Claude Code, Claude Desktop, other agents) can manage and restore backups with typed, schema-validated tools:

bash
# Register with Claude Code (point --dir at the directory holding backup_jobs.yaml)
claude mcp add baki -- /path/to/baki mcp --dir /path/to/config-dir

Tools: baki_list_jobs, baki_job_status, baki_add_job, baki_edit_job, baki_remove_job, baki_run_job, baki_list_snapshots, baki_search_backups, baki_restore.

  • Read-only tools (list_jobs, job_status, list_snapshots, search_backups) and restores work without the baki server running; baki_run_job only queues a trigger — the server executes it, and the result reports server_up so the agent knows whether it actually will.
  • edit_job, remove_job, and restore carry MCP destructive annotations, so well-behaved clients ask the human before running them.
  • Results include both a human-readable summary and structured JSON content.

Remote access (Streamable HTTP)

For network clients (e.g. a bot on another host), serve the same tools over Streamable HTTP instead of stdio:

bash
baki mcp-token add my-agent --url https://your-host:8443   # create a token first
./baki mcp --transport http --addr 0.0.0.0:8443 \
    --tls-cert cert.pem --tls-key key.pem --dir /path/to/config-dir
  • Endpoint: POST https://<host>:8443/mcp (standard MCP Streamable HTTP: initialize, tools/list, tools/call; session ids and SSE handled per spec). Health check: GET /healthz → 200, no auth.

  • Bearer auth is mandatory — every /mcp request needs Authorization: Bearer <token>; anything else gets 401, and the server refuses to start in http mode with no tokens configured, since these tools can trigger restores.

  • Per-agent tokens — give each agent its own token instead of sharing one, so you can revoke agents individually:

    bash
    baki mcp-token add discord-bot --url https://backup.example.lan:8443
    # prints the token ONCE plus ready-to-paste config (endpoint, auth header,
    # 'claude mcp add' command, generic JSON client config)
    baki mcp-token list
    baki mcp-token remove discord-bot   # takes effect immediately, no restart
    

    Tokens are stored as SHA-256 hashes in .mcp_tokens.json (0600, gitignored) next to backup_jobs.yaml; the token file is re-read per request, so add/remove apply to a running server without a restart.

  • --addr defaults to 127.0.0.1:8443; binding to all interfaces is an explicit choice (0.0.0.0:8443).

  • Without --tls-cert/--tls-key it serves plain HTTP and logs a warning — only do that behind a TLS-terminating reverse proxy, as the token travels in the header.

  • Tool names, schemas, annotations, and results are identical to the stdio transport.

Backing Up over WireGuard

If your source hosts live behind a WireGuard VPN, baki can join this machine to an existing WireGuard server automatically — the same trust model as backups (SSH):

bash
sudo baki vpn join --server [email protected] [--iface wg0] [--local-iface wg-baki] \
     [--ssh-key ~/.ssh/baki] [--ssh-port 22]

This generates a WireGuard keypair locally, reads the server's interface over SSH (wg show ... dump), allocates the next free VPN address, registers this machine as a peer (wg set + wg-quick save), writes /etc/wireguard/wg-baki.conf (0600), and enables a persistent wg-quick@wg-baki systemd service — then verifies the tunnel with a ping. From there, jobs just use WireGuard IPs:

bash
baki add --name inner-host --user kiki --host 10.8.0.7 --key ~/.ssh/baki --source /home/kiki --dest ~/backups/inner
  • wirebard-managed servers (multi-network setups with isolated/full-proxy networks, see wirebard): let wirebard own the policy — baki asks it for a peer and receives a ready client config:

    bash
    baki vpn networks --server [email protected]          # what's on offer
    sudo baki vpn join --server [email protected] --provisioner wirebard --network backups
    sudo baki vpn leave --server [email protected] --provisioner wirebard --network backups
    

    In this mode wirebard allocates the address and decides AllowedIPs/DNS/MTU per network type; baki only generates the keypair locally (the private key never leaves the machine — configs arrive with a {{PRIVATE_KEY}} placeholder and are refused without it) and manages the local tunnel service.

  • sudo baki vpn status — interface state and last handshake.

  • sudo baki vpn leave --server ... — removes the peer from the server and tears down the local tunnel.

  • Requirements: wireguard-tools on both ends; the SSH user on the server must be root or have passwordless sudo for wg/wg-quick. The tunnel survives reboots; if provisioning fails midway, the half-added peer is rolled back on the server.

Service Mode

Run the application with the server subcommand. It's recommended to run it in the background using nohup or a process manager like systemd.

bash
nohup ./baki server &

In service mode, Baki will:

  1. Load jobs from backup_jobs.yaml.
  2. Initialize status files in .backup_status.
  3. Schedule jobs according to their cron expressions.
  4. Watch backup_jobs.yaml for changes and reload/reschedule automatically.
  5. Periodically check the .backup_triggers directory for trigger files (e.g., trigger_jobname_12345) and execute the corresponding job if found and not already running.
  6. Log activities to backup-tui-debug.log.

To stop the service if run with nohup, find its process ID (PID) and use kill <PID>. See the next section for running with systemd.

Running as a Systemd Service

The easy way — from the directory containing backup_jobs.yaml:

bash
sudo ./baki service-install    # writes /etc/systemd/system/baki.service, enables + starts it
sudo ./baki service-remove     # stops and removes it again (data/config untouched)

service-install fills in the unit automatically: it runs the service as the sudo caller (--user overrides), uses the current directory as WorkingDirectory (--dir overrides), and points ExecStart at the invoked binary. It warns if a leftover nohup server instance is still running.

Alternatively, ready-to-edit unit files are in examples/:

  • examples/baki.service — system-level service (starts at boot, runs as a dedicated user):

    bash
    sudo cp examples/baki.service /etc/systemd/system/baki.service
    # edit User/Group/WorkingDirectory/ExecStart for your setup, then:
    sudo systemctl daemon-reload
    sudo systemctl enable --now baki.service
    sudo journalctl -u baki.service -f   # follow logs
    
  • examples/baki.user.service — per-user service, no root needed:

    bash
    mkdir -p ~/.config/systemd/user
    cp examples/baki.user.service ~/.config/systemd/user/baki.service
    systemctl --user daemon-reload
    systemctl --user enable --now baki.service
    loginctl enable-linger $USER   # keep it running while logged out
    

WorkingDirectory must be the directory containing backup_jobs.yaml; the status/trigger directories and the log file live there too.

Configuration (backup_jobs.yaml)

Jobs live in backup_jobs.yaml in the directory where you run baki (or the systemd WorkingDirectory). The easiest way to manage it is baki add/edit/remove or the TUI; to write it by hand, start from backup_jobs.yaml.example. The file is gitignored — it describes your infrastructure, so keep it out of version control.

Example:

yaml
- id: my_server_home_1678886400
  name: My Server Home Backup
  user: backupuser
  host: server.example.com
  port: "22" # Optional, defaults to 22
  key_path: "~/.ssh/id_baki" # Optional, defaults to ~/.ssh/id_rsa
  source: /home/user/data/ # Trailing slash is recommended for rsync directory contents
  destination: /mnt/backups/my_server/home
  schedule: "0 3 * * *" # Optional, cron schedule (runs daily at 3:00 AM)
  retention: 14 # Optional, snapshots to keep (default 7)
- id: another_project_1678886401
  name: Another Project Files
  user: projectsync
  host: project.internal
  source: /srv/project/files/
  destination: /data/backups/project_files
  schedule: "0 0 * * 1" # Optional, runs weekly on Monday at midnight

Fields:

  • id: (Optional, auto-generated if missing) A unique identifier for the job. Used internally for status/trigger files.
  • name: A user-friendly name for the job displayed in the TUI. Required.
  • user: The SSH username on the remote host. Required.
  • host: The hostname or IP address of the remote host. Required.
  • port: (Optional) The SSH port on the remote host. Defaults to 22.
  • key_path: (Optional) The path to the SSH private key file. Supports ~/ expansion. Defaults to ~/.ssh/id_rsa.
  • source: The remote source directory path on the host. Required. A trailing slash (/) is significant for rsync (copies contents vs. the directory itself).
  • destination: The local destination directory path. Required. Created if it doesn't exist. Snapshots are written into dated subdirectories here (see below).
  • schedule: (Optional) A standard cron expression (minute hour day-of-month month day-of-week). If omitted, the job will only run via manual triggers.
  • retention: (Optional) Number of daily snapshots to keep as plain directories. Defaults to 7.
  • retention_weekly: (Optional) Number of weekly SquashFS archives to keep. 0 (default) disables the weekly tier.
  • retention_monthly: (Optional) Number of monthly SquashFS archives to keep. 0 (default) disables the monthly tier.

Snapshots and Retention

Each run creates a new point-in-time snapshot instead of overwriting the previous backup:

code
destination/
├── 2026-07-10_06-00-01/
├── 2026-07-11_06-00-00/
├── 2026-07-12_06-00-00/
└── latest -> 2026-07-12_06-00-00
  • Every run rsyncs into a new YYYY-MM-DD_HH-MM-SS directory and uses --link-dest against the previous snapshot, so unchanged files are hard links — each additional snapshot only consumes the space of what changed since the last run.
  • latest is a symlink to the most recent successful snapshot; restoring "the current state" means copying from latest/.
  • After each successful run, snapshots beyond the job's retention count (default 7) are deleted, oldest first.
  • A failed run's partial snapshot directory is removed automatically; latest keeps pointing at the last good snapshot, and the next run hard-links against it.

Tiered Retention (weekly/monthly archives)

With retention_weekly and/or retention_monthly set (requires squashfs-tools installed), aged-out daily snapshots are not simply deleted: the first snapshot of each new ISO week/month is first compressed into a SquashFS archive under destination/archive/:

code
destination/
├── 2026-07-06_06-00-00/ ... 2026-07-12_06-00-00/   # daily snapshots (plain dirs)
├── latest -> 2026-07-12_06-00-00
└── archive/
    ├── weekly_2026-W27_2026-06-29_06-00-00.sqsh
    ├── weekly_2026-W27_2026-06-29_06-00-00.manifest.gz
    ├── monthly_2026-06_2026-06-01_06-00-00.sqsh
    └── monthly_2026-06_2026-06-01_06-00-00.manifest.gz
  • Each archive is a compressed, mountable SquashFS image plus a gzipped manifest (file list with sizes and mtimes), so archived contents can be listed and searched instantly without decompressing.
  • Archives beyond their retention counts are deleted, oldest first. If archive creation fails (e.g. mksquashfs missing), the snapshot directory is kept and archiving is retried on the next run — data is never deleted before its archive exists.
  • baki search JOB PATTERN finds files across all daily snapshots (directory walk) and all archives (manifest scan); baki restore JOB weekly_2026-W27 [--file ...] extracts from an archive via unsquashfs.
  • Note: archives trade the hard-link dedup of plain snapshots for compression and single-file portability. For pure disk savings with zero workflow change, filesystem-level compression (btrfs/ZFS zstd) on the destination is a good alternative or complement.

SquashFS Cheat Sheet

Baki drives mksquashfs/unsquashfs for you, but the archives are standard SquashFS images you can work with directly (sudo pacman -S squashfs-tools on Arch, apt install squashfs-tools on Debian/Ubuntu). A=destination/archive/weekly_2026-W27_2026-06-29_06-00-00.sqsh in the examples below.

bash
# List every file inside an archive (instant — reads the index, not the data)
unsquashfs -ll "$A"

# Search inside an archive without Baki
unsquashfs -ll "$A" | grep -i report

# Extract the whole archive to a directory
unsquashfs -d /tmp/restore "$A"

# Extract only specific files/dirs (paths as shown by -ll, relative to the root)
unsquashfs -d /tmp/restore "$A" docs/report.txt photos/2026/

# Browse an archive in place — mount it read-only, no extraction at all
sudo mount -t squashfs -o loop,ro "$A" /mnt
ls /mnt && cp /mnt/docs/report.txt ~ && sudo umount /mnt

# Show archive info: compression algorithm, block size, original vs compressed size
unsquashfs -stat "$A"

# What Baki runs to create an archive (zstd compression)
mksquashfs destination/2026-06-29_06-00-00 "$A" -comp zstd -quiet -no-progress

# Better compression at the cost of speed (if creating archives manually)
mksquashfs SRC OUT.sqsh -comp zstd -Xcompression-level 19

# Verify an archive is intact (full test extraction, then discard)
unsquashfs -d "$(mktemp -d)/check" "$A" > /dev/null && echo OK

# Read Baki's manifest without any squashfs tooling
zcat "${A%.sqsh}.manifest.gz" | less     # columns: size, mtime (unix), path

Tips:

  • unsquashfs refuses to overwrite an existing target directory unless you pass -f; Baki's restore passes it for you.
  • Mounting is the fastest way to poke around a big archive interactively — nothing is decompressed until you actually read a file.
  • Archives are immutable. To "change" one you extract, modify, and re-create — but for backups you should never need to.

Status Files (.backup_status/)

The service mode (and TUI indirectly) reads/writes status information for each job into JSON files within the .backup_status directory (created automatically in the working directory). Each file is named job_status_<safe_job_id>.json. These files track the current status, progress (rudimentary 0.0 or 1.0), start/end times, and last error messages.

Trigger Files (.backup_triggers/)

When you select 'Run Now' ('r') in the TUI, it creates an empty file in the .backup_triggers directory (created automatically in the working directory, e.g., trigger_my_server_home_1678886400). The service mode periodically scans this directory. If it finds a trigger file for a job that isn't currently running, it executes the job and removes the trigger file.

Logging (backup-tui-debug.log)

Both TUI and service modes log detailed information, including errors, scheduling actions, rsync commands, and status updates, to backup-tui-debug.log in the application's working directory.

Dependencies

This application uses the following Go modules:

  • github.com/charmbracelet/bubbles
  • github.com/charmbracelet/bubbletea
  • github.com/charmbracelet/lipgloss
  • github.com/fsnotify/fsnotify
  • github.com/robfig/cron/v3
  • gopkg.in/yaml.v3

Dependencies are managed using Go modules and will be downloaded automatically during the build process.

License

This project is licensed under the MIT License.

> GERMAN CLIMATE REPORT PROJECTS 3°C WARMING BY 2050: ANALYSIS OF NEW PROJECTIONS> TAILSCALE SSH INSECURE ARGUMENT HANDLING ALLOWS ROOT ACCESS VIOLATION> OPENAI RESEARCHER MILES WANG EXPLORES LAUNCH OF AI-DRIVEN DRUG DISCOVERY STARTUP> DATA CENTERS' ELECTRICITY COST INCREASES IMPOSED ON PUBLIC VIA $23 BILLION SURCHARGE> SOLVING 27 ERDŐS PROBLEMS USING PARALLEL AI-DRIVEN LEAN 4 AGENTS> VANCOUVER POLICE DEPARTMENT LAUNCHES QUICK ESCAPE FEATURE FOR WEBSITE PRIVACY> LORDE CRITICIZES META'S AI GLASSES AS 'NOT SEXY' AND RAISES PRIVACY CONCERNS> TRAINING A JEPA WORLD MODEL ON SUPER MARIO BROS FOR DYNAMIC PREDICTION AND PLANNING> OPENAI'S SCREENLESS, MOBILE AI SPEAKER WITH AUTONOMOUS MOVEMENT REVEALED> OPENAI DENIES ALLEGATIONS IN APPLE'S TRADE SECRET LAWSUIT> GERMAN CLIMATE REPORT PROJECTS 3°C WARMING BY 2050: ANALYSIS OF NEW PROJECTIONS> TAILSCALE SSH INSECURE ARGUMENT HANDLING ALLOWS ROOT ACCESS VIOLATION> OPENAI RESEARCHER MILES WANG EXPLORES LAUNCH OF AI-DRIVEN DRUG DISCOVERY STARTUP> DATA CENTERS' ELECTRICITY COST INCREASES IMPOSED ON PUBLIC VIA $23 BILLION SURCHARGE> SOLVING 27 ERDŐS PROBLEMS USING PARALLEL AI-DRIVEN LEAN 4 AGENTS> VANCOUVER POLICE DEPARTMENT LAUNCHES QUICK ESCAPE FEATURE FOR WEBSITE PRIVACY> LORDE CRITICIZES META'S AI GLASSES AS 'NOT SEXY' AND RAISES PRIVACY CONCERNS> TRAINING A JEPA WORLD MODEL ON SUPER MARIO BROS FOR DYNAMIC PREDICTION AND PLANNING> OPENAI'S SCREENLESS, MOBILE AI SPEAKER WITH AUTONOMOUS MOVEMENT REVEALED> OPENAI DENIES ALLEGATIONS IN APPLE'S TRADE SECRET LAWSUIT