What is Sentra?
Sentra is a single Go binary (sentra) that backs up local directories to
S3 or any S3-compatible store as encrypted, content-addressed snapshots. It
runs equally well as a scriptable CLI or a full-screen TUI, and it ships with a
built-in agent that audits your repository and surfaces recommendations.
- π Client-side encryption. New blobs are sealed with XChaCha20-Poly1305
using a per-blob 24-byte random nonce; the data key is derived from your
passphrase with Argon2id. The bucket only ever sees ciphertext.
- π§© Content-defined dedup. FastCDC chunking + SHA-256 content addressing.
A 50 GiB tree with one changed file uploads roughly 1 MiB on the next
snapshot.
- ποΈ Versioned snapshots. Each snapshot is an immutable, encrypted manifest
pointing at chunk hashes. Restore re-derives and verifies every hash, so it is
exact-byte by construction.
- π€ A hybrid agent. Local heuristics run first; the optional LLM sees
summaries only β never file contents or secret values. Recommendations are
read-only by default (prune candidates, ignore-list additions, secret
findings, retention drift).
- π MCP built in.
sentra mcp serves your snapshots to Claude β or any
MCP client β over stdio: metadata only, with two-phase confirm-gated
mutations. Inside the TUI, ctrl+a opens a built-in assistant chat with
the same guarantees.
- π A synthwave TUI.
sentra ui β or just sentra β opens a Bubbletea app
that lands on a first-run wizard, an unlock gate, or the dashboard depending on
your state.
A quick tour
Snapshots β sort, filter, drill in; r restores and d diffs the highlighted row.
|
Backup β a three-step wizard: pick a folder (with jump-to places), choose a schedule, confirm with a tag.
|
First-run wizard β guided setup for AWS or any S3-compatible store.
|
Settings β config summary plus scheduled backups, recovery kit, passphrase, and setup.
|
Backup, step 2 β one-shot, or a cadence that installs a named policy and an OS timer; it then shows up under Schedules.
|
Install
go install
go install github.com/markgustetic/sentra/cmd/sentra@latest
Builds from source with your Go toolchain (1.27+); sentra version reports
the module version and commit from the embedded build info.
Homebrew (from tagged releases)
brew install markgustetic/tap/sentra
Prebuilt binaries (signed) (from tagged releases)
Download platform archives (macOS and Linux, amd64/arm64) from the
releases page. Each release
ships a checksums.txt plus a cosign keyless signature (checksums.txt.sig and
checksums.txt.pem) and a syft SBOM per archive. The certificate identity is
the exact release-workflow path bound to a tag ref β substitute the version you
downloaded:
cosign verify-blob \
--certificate checksums.txt.pem \
--signature checksums.txt.sig \
--certificate-identity 'https://github.com/markgustetic/sentra/.github/workflows/release.yml@refs/tags/<version>' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
Quickstart
π Fastest: kick the tires with local MinIO (no AWS account)
sentra local starts a local MinIO via docker compose, points Sentra at a
throwaway .sentra-local.yaml (never your real config), and opens the TUI with
the first-run wizard pre-filled for MinIO. It runs docker compose in the
current directory, so run it from a clone of this repo (which carries the
docker-compose.yaml):
git clone https://github.com/markgustetic/sentra && cd sentra
just local # builds the binary and opens the TUI (needs Docker)
just local-reset wipes everything back to a clean first run.
πͺ£ Real storage: the setup wizard
For AWS S3 or any S3-compatible store, let the wizard do the work:
sentra setup
It walks you through the backend choice, can create or verify the bucket,
block public access, enable default encryption, write sentra.yaml, and
initialize the encrypted repo β all in one flow, reviewing a non-secret plan
before it touches anything.
-
AWS S3 β sign in with AWS CLI browser login (the default), IAM Identity
Center / SSO, an existing profile/role, or write config only. After a
browser or SSO sign-in the wizard offers to create a dedicated backup
user β a scoped IAM user whose static keys never expire β and switches
your config to it, so scheduled backups survive the night. Browser login
alone is for trying Sentra: its session expires within hours. Need an admin
to grant permissions first? The wizard can print the least-privilege IAM
policy and stop:
sentra setup iam-policy --bucket my-backups --prefix sentra/
-
S3-compatible or existing bucket (MinIO, LocalStack, Cloudflare R2,
Wasabi, β¦) β enter the bucket, region, and endpoint_url and you're set.
When it initializes the repo, Sentra asks you to set a repository passphrase
(unless --passphrase-file or SENTRA_PASSPHRASE supplies one). Choose Save
in keychain and Sentra stores it in your OS keyring, writing only
passphrase.use_keyring: true to sentra.yaml β never the secret itself.
[!NOTE]
No secrets are ever written to sentra.yaml, logs, setup drafts, or
recovery kits. Not the passphrase, not wrapped keys, not AWS credentials.
πΈ Take a snapshot
sentra backup ./Documents --tag weekly
Repeating a backup? Save the path and maintenance choices as a policy, then
install it into your OS user scheduler (launchd on macOS, systemd user
timers on Linux β no resident daemon):
sentra policy add home --path ./Documents --tag home --schedule daily@03:00 --check --prune dry-run
sentra policy run home
sentra schedule install home
sentra schedule status home
schedule install writes the LaunchAgent plist (macOS) or the systemd user
service + timer (Linux) and loads it right away β launchctl bootstrap gui/$UID β¦ or systemctl --user enable --now β¦ β so nothing waits for your
next login. schedule status asks the OS whether the timer is actually
loaded, and when it is not (a headless SSH session has no user session to
load into) it prints the exact command to run once you have one.
schedule uninstall unloads the timer before removing its files.
A slot that passes while the machine is asleep fires when it wakes, on
both platforms. One that passes while it is shut down is caught up at
your next login: the timer also runs sentra policy run --if-due at load,
which backs up only if no snapshot of the policy exists since its most
recent scheduled slot and otherwise exits with "not due until β¦". Loading
at install counts too, so a brand-new schedule takes its first backup right
after schedule install. schedule status and the Schedules view show the
computed next slot, not the catch-up.
Want a reviewed, two-step run? Write a JSON plan, inspect it, then apply:
sentra backup plan ./Documents --tag weekly --out weekly-plan.json
sentra backup apply weekly-plan.json
π List, restore, verify
sentra snapshots # newest first; --json for scripting
sentra restore <snapshot-id> /tmp/out --dry-run # preview, writes nothing
sentra restore <snapshot-id> /tmp/out --verify # restore + re-check chunk hashes
sentra check # audit manifests, chunk refs, orphans, stale locks
sentra recovery-kit --out sentra-recovery-kit.md # non-secret restore notes
π€ Ask the agent
sentra agent advise-ignore ./Documents # suggest .sentraignore patterns (read-only)
sentra agent scan --local-only --root ./Documents # heuristics only, no LLM
sentra agent scan --apply # review + apply recommendations interactively
π Serve it to your AI (MCP)
sentra mcp runs a Model Context Protocol
server on stdio, so Claude (or any MCP client) can work with your backups β
metadata only, never file contents, and every mutation is two-phase:
the client gets a plan and a single-use token, and nothing runs until the
matching confirm_* call.
claude mcp add sentra -- sentra mcp
Tools: list_snapshots, snapshot_files, find, diff_snapshots,
repo_stats, plus plan_backup/confirm_backup and
plan_restore/confirm_restore. The passphrase must resolve
non-interactively (keyring, SENTRA_PASSPHRASE, or --passphrase-file) β
stdio belongs to the protocol.
The TUI carries its own assistant with the same boundary: ctrl+a opens a
chat overlay (needs ANTHROPIC_API_KEY) whose actions compile into the exact
flows the keyboard drives β a chat-requested backup raises the same confirm
dialog you'd get pressing enter yourself.
The full walkthrough lives in docs/QUICKSTART.md.
The TUI
Bare sentra falls through to sentra ui. Where it lands depends on your state:
no sentra.yaml β first-run wizard, configured but locked β unlock gate,
otherwise the dashboard. The rail holds seven destinations β Dashboard,
Backup, Schedules, Snapshots, Maintenance, Settings, Help β and the
occasional jobs live one keypress inside them: restore and diff launch from a
snapshot row, check/prune/sync/doctor from Maintenance, recovery-kit/
passphrase/setup from Settings. Backup is a three-step wizard: pick the
folder, pick a schedule (one-shot, or hourly/daily/weekly/monthly with a
time, weekday and editable policy name; a chosen cadence installs a named
policy plus a launchd/systemd timer), confirm.
Handy keys (the status bar always shows what's live):
| Key |
Action |
β / β Β· digits |
Move the nav rail Β· jump straight to a view |
tab |
Toggle focus between the rail and the content pane |
enter Β· esc |
Trigger the primary action Β· go back |
ctrl+p |
Command palette |
ctrl+a |
Assistant chat β ask questions, or say what to do (actions still confirm) |
r Β· d (in Snapshots) |
Restore Β· diff the highlighted snapshot |
enter Β· esc (in Backup) |
Next wizard step Β· back a step |
enter Β· e Β· d (in Schedules) |
Drill into a job's files Β· edit Β· delete (policy + timer) |
? Β· q |
Help Β· quit |
Selection is carried by a β glyph, not just color, and the neon strips
cleanly under NO_COLOR, a pipe, or a 2-color terminal β the synthwave look is
a dark-terminal flourish, never a legibility risk.
Commands
| Command |
What it does |
sentra ui |
Launch the full-screen TUI. Bare sentra is equivalent. |
sentra local |
Dev flow: start local MinIO and open the wizard-prefilled TUI. |
sentra setup |
Guided wizard for AWS/S3 config, bucket prep, and repo init. |
sentra setup iam-policy |
Print non-secret AWS IAM JSON for a bucket/prefix. |
sentra doctor |
Check config, AWS access, bucket settings, and repo health β read-only. |
sentra init |
Non-interactively create sentra.yaml and the encrypted repo config. |
sentra backup <path> |
Snapshot a directory now. --tag labels it. plan/apply for reviewed runs. |
sentra snapshots |
List snapshots, newest first. --json for scripting. |
sentra ls <snap> |
List a snapshot's tree: files, dirs, symlink targets. |
sentra restore <snap> <dest> [pathβ¦] |
Restore a snapshot β or just the named files/subtrees. --dry-run previews; --verify validates output. |
sentra diff <a> <b> |
Show added / removed / changed paths between two snapshots. |
sentra check |
Audit manifests, chunk references, orphan blobs, and stale locks. --read-data re-downloads and re-hashes chunks. |
sentra stats |
Dedup factor, logical vs stored bytes, per-snapshot unique footprint. |
sentra pin / unpin |
Protect a snapshot from prune and deletion. |
sentra prune |
Dry-run retention by default; --apply reclaims, --explain shows reasons. |
sentra policy β¦ |
Manage named backup policies (add/list/show/remove/run); removing a policy also uninstalls its OS timer. |
sentra schedule β¦ |
Install user-level OS schedules for named policies. |
sentra password |
Rotate or forget the repository passphrase (passwd is an alias). |
sentra sync --dst-config |
Replicate this repo to a clone destination (additive; --snapshot selects a subset). |
sentra recovery-kit |
Export non-secret recovery notes and restore commands. |
sentra agent advise-ignore |
Suggest first-run .sentraignore patterns without editing files. |
sentra agent scan |
Heuristics + optional LLM. --local-only, --root, --categories, --apply. |
Every subcommand resolves the passphrase in this order:
--passphrase-file β SENTRA_PASSPHRASE β OS keyring (when
passphrase.use_keyring: true) β interactive prompt (TTY only). Keyring
entries are scoped to the configured bucket and prefix, so multiple repos can
safely share one bucket under different prefixes.
Configuration
Sentra looks for sentra.yaml in the current directory first, then falls
back to ~/.config/sentra/sentra.yaml (honoring XDG_CONFIG_HOME).
First-run setup writes the home location, so after setting up once you
can run sentra from any directory. Pass --config to use a specific
file; it must already exist (sentra setup --config <path> creates one).
sentra.yaml holds non-secret settings only. A .sentraignore at the walk root
applies gitignore-style globs (a starter ships at
.sentraignore.example). The Anthropic provider needs
ANTHROPIC_API_KEY; without it every non-agent command still works and
sentra agent scan returns a clear error.
repo:
s3:
bucket: my-backups # required
prefix: sentra/ # optional β lets multiple repos share a bucket
region: us-west-2 # optional β falls back to the AWS SDK chain
profile: default # optional β AWS shared-credentials profile
endpoint_url: "" # optional β set for MinIO / LocalStack / R2 / Wasabi
storage_class: "" # optional β e.g. STANDARD_IA or GLACIER_IR
agent:
provider: anthropic
model: claude-sonnet-4-6
max_findings_to_llm: 50 # cap on prompt size
backup:
ignore_file: .sentraignore
exclude_caches: true # honor CACHEDIR.TAG
concurrency: 0 # 0 = auto; parallel file scan/upload workers
max_upload_rate: "" # optional throttle, e.g. 10MiB
ui:
hide_splash: false # skip the launch splash
notify:
disable_desktop: false # true silences the after-run desktop notification
retention:
keep_last: 10
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
passphrase:
use_keyring: false # true means future commands read the OS keyring
policies:
home:
paths: ["~/Documents"]
tags: ["home"]
schedule: { cadence: daily, at: "03:00" }
after_backup: { check: true, prune: dry-run } # prune: off | dry-run | apply
hooks: # optional, run via `sh -c`
before: pg_dump mydb > ~/Documents/db.sql # a failure aborts the run
after: echo done # runs only after a fully successful run
on_failure: say "backup failed" # runs when any stage fails
on_failure_webhook_env: SENTRA_ALERT_URL # env var NAME holding a URL to POST to
Notifications and hooks
Every backup run announces its result on the desktop β Backup complete
with the file count and new bytes, or Backup failed with the error β
whether you ran it from the TUI, from sentra policy run, or the OS timer
ran it at 3am with no terminal attached. It is on by default with nothing to
set up; switch it off with the Desktop notifications toggle in Settings
or notify.disable_desktop: true. macOS posts through osascript, Linux
through notify-send; the first notification from a timer on macOS may ask
you once to allow notifications from Script Editor. The ad-hoc sentra backup
command stays quiet, since its result prints to the terminal you ran it in.
For anything beyond a notification, a policy's hooks run your own commands
around the run, and on_failure_webhook_env names an environment variable
whose URL receives a {policy, status, error} JSON POST on failure β only the
variable's name goes in sentra.yaml, never the URL.
Security
Sentra is a backup tool, so it's worth being explicit about what the encryption
protects β and what it doesn't. The threat model has the
full write-up; the load-bearing invariants:
- Encryption. New blobs use XChaCha20-Poly1305 with a per-blob 24-byte random
nonce and the version byte bound as associated data. A nonce is never reused
under a key, and the bucket never sees plaintext.
- Content addressing. A chunk's key is the SHA-256 of its raw (decompressed)
plaintext. Restore re-derives and checks it on read, so restore is exact-byte.
- GC safety. GC computes its live set from the snapshots present in the store
under the repo lock β never from a caller-supplied set β so a blob referenced by
any present manifest is never reaped.
- One repo lock.
backup, GC, sync, passwd, and snapshot-apply
serialize on an advisory meta/lock (atomic put-if-absent). Release is
fail-closed.
- Agent boundary. The LLM sees summaries only, never file contents or secret
values; recommendations are read-only by default.
Protects: file contents, file paths, manifest metadata, snapshot tags.
Leaks: object counts and approximate sizes (compress-then-encrypt), S3 access
logs. Out of scope: forward secrecy across passphrase compromise, post-quantum
key strength.
Architecture
See docs/architecture.md for the storage model, the
agent loop, and Mermaid diagrams of the backup, restore, and agent scan
flows. In one breath: a snapshot is an encrypted manifest of SHA-256 chunk hashes;
chunks are FastCDC-split, zstd-compressed, then sealed; dedup falls out of content
addressing; and both the CLI and TUI drive the same core in internal/repo.
Development
Go 1.27+ is required, and the codebase is internal/-only (no public API in
v1). Prefer just:
just build # β bin/sentra
just local # build + open the TUI against local MinIO (the easy path)
just check # build, race tests, vet, lint, vuln, tidy/gofmt/diff checks
just test # go test -race with coverage
just integration # testcontainers + MinIO (needs Docker; Linux)
The vendored FastCDC is a separate module β test it with
go test ./third_party/fastcdc-go/... when you touch chunking. CI enforces
go mod tidy -diff, gofmt over cmd/ and internal/, go vet, go test -race,
the FastCDC module tests, golangci-lint, the integration suite
(testcontainers + MinIO), and an every-commit-builds job that checks out and
compiles each pushed commit in isolation β a green run vouches for the whole
series, not just the tip.
Releasing
Push a v* tag to trigger release.yml:
goreleaser cross-compiles linux/darwin Γ amd64/arm64 archives, writes a
SHA-256 checksums.txt, signs it with cosign keyless (GitHub OIDC), updates
the Homebrew tap, and attaches a syft SBOM per archive. The Homebrew step publishes only when the HOMEBREW_TAP_TOKEN secret
(contents: write on markgustetic/homebrew-tap) is present β without it the
cask is skipped and everything else still ships. GITHUB_TOKEN is provided
automatically.
License
MIT.
β¦ built to keep your bytes safe, and to look good doing it β¦