Local-first backup coverage, verification, restore planning, and AWS backup posture audits for agents and operators.
open-backup is the control layer around proven backup targets. It does not try to replace S3, AWS Backup, RDS snapshots, or local archives. It answers the operational questions those systems leave scattered:
- What important data is covered?
- Where is it backed up?
- When was the last successful run?
- Can the artifact be verified?
- What would restore touch before anything is applied?
- Which AWS resources have weak backup posture?
| Doc | Contents |
|---|---|
| docs/COMMANDS.md | CLI command reference |
| docs/DESTINATIONS.md | Destination capability matrix (local / S3 / SFTP / B2) |
| docs/AWS.md | AWS audit guide — checks, IAM, interpreting findings |
| docs/RESTORE.md | Restore runbook — plan-only workflow and manual apply |
| docs/RUNTIME_CONTRACTS.md | Runtime backup contract schema |
bun install -g @hasna/backupbackup init
backup sources add ~/.hasna --name hasna-state
backup destinations add local ~/.hasna/backup/local --name local
backup plan
backup run
backup prune --plan
backup run --contract
backup verify latest
backup verify latest --contract
backup evidence-ref latest --source hasna-state
backup restore plan latest --target /tmp/open-backup-restore
backup restore apply latest --target /tmp/open-backup-restore --yes
backup restore plan latest --target /tmp/open-backup-restore --contractbackup destinations add s3 s3://my-backups/laptop --name s3-main --aws-profile default --region us-east-1
backup run --destination s3-main
backup verify latestS3 support uses the installed AWS CLI and explicit named profiles. open-backup does not store AWS credentials.
For S3-compatible providers such as Backblaze B2, MinIO, or Wasabi, add --endpoint <url> to the S3 destination:
backup destinations add s3 s3://my-b2-bucket/laptop --name b2-main --aws-profile b2 --region us-west-004 --endpoint https://s3.us-west-004.backblazeb2.comRead-only AWS posture checks are available for backup buckets, AWS Backup, RDS/Aurora, EBS, DynamoDB, and EFS:
backup aws s3-audit --profile hasna-xyz-infra --bucket hasna-xyz-infra-backups-prod
backup aws backup-audit --profile hasna-xyz-infra --regions us-east-1
backup aws rds-audit --profile hasna-xyz-infra --regions us-east-1,eu-central-1
backup aws ebs-audit --profile hasna-xyz-infra --regions us-east-1 --snapshot-age-days 7
backup aws dynamodb-audit --profile hasna-xyz-infra --regions us-east-1
backup aws efs-audit --profile hasna-xyz-infra --regions us-east-1
backup aws inventory --profile hasna-xyz-infraRuntime contracts describe cloud-backed app state before any backup or restore is run:
backup runtime-contract validate runtime-contract.json
backup runtime-contract plan runtime-contract.json --home ~/.hasna/backupThe validator distinguishes local SQLite and local file state from remote Postgres/RDS and S3 state. Local stores are treated as filesystem backup sources. Remote Postgres/RDS and S3 stores are treated as metadata and posture contracts only; validation does not connect to databases, create snapshots, upload objects, create buckets, change AWS settings, run Terraform, or apply restores. Contract findings reject credential-bearing fields and do not echo secret values.
See Runtime Backup Contracts for the schema and examples.
backup init
backup doctor
backup inventory
backup status
backup sources list|add|remove|inspect
backup destinations list|add|remove|test|inspect
backup policy show|set
backup plan
backup run
backup prune --plan|--apply
backup list
backup hold <backup-id|latest> [--reason label] [--note text]
backup unhold <backup-id|latest>
backup holds
backup show <backup-id|latest>
backup manifest <backup-id|latest>
backup verify <backup-id|latest>
backup evidence-ref <backup-id|latest> [--source name]
backup restore plan <backup-id|latest> --target <path>
backup restore apply <backup-id|latest> --target <path> --yes
backup runtime-contract validate|plan <file>
backup aws inventory|s3-audit|backup-audit|rds-audit|ebs-audit|dynamodb-audit|efs-audit
backup-mcp
All CLI commands emit JSON by default so humans, agents, and CI can consume the same output. --contract on run, plan, verify, and restore plan emits the corresponding @hasna/contracts work run, proof bundle, or validation plan. evidence-ref emits an @hasna/contracts evidence reference for a backup manifest or source archive.
- Restore apply is dry-run by default. It prints the pre-flight file diff and writes only with
--applyor--yes. - Existing restore paths are never overwritten unless
--yesis provided. - Prune is plan-only by default.
backup prune --applydeletes only the artifacts listed in that same prune plan. backup hold <backup-id>pins a backup so prune can never expire it, whatever the policy says. Held backups are listed with ahold:<reason>retention reason, and an unreadable hold store makes prune fail instead of expiring pinned backups.- Secrets are excluded by default through archive exclusions.
- Archives are not encrypted by this tool.
policy.encryptionis alwaysnone; there is no encryption or key-management option, and none of thetgzarchives written to a local, S3, or SFTP destination are encrypted or signed bybackup. Any protection at rest comes entirely from the destination: bucket-level SSE and versioning protect against media theft and accidental overwrite, but not against any principal that can read the objects — anyone holdings3:GetObject(or filesystem read access, or the SFTP account) can read every archive in plaintext. Do not describe these archives as encrypted. Restrict read access at the destination and treat archive contents as sensitive. .gitis excluded by default, so an archive of a repository working tree carries no commit history.sources addandrunwarn about that,runrecordsgitHistoryExcludedon the affected manifest archive entry, anddoctorreports it. Passbackup sources add <path> --include-git-historyto keep the object database while every other default exclude (secrets patterns,node_modules, build output) stays in force — that exclude list is derived from the defaults, so it cannot drift from them. On one machine 31 of 34 registered sources carried this exclude and no source targeted any object database, so no git history was captured at all while coverage reported green; seedocs/GIT_HISTORY.md. Where the objects live outside the source path (a linked worktree), back up commit history as agit bundle create <file> --allorgit clone --mirrorartifact instead — and verify that artifact by cloning from it, becausegit bundle verifyreports "The bundle records a complete history." for bundles of shallow repositories that then fail to clone.- The
.gitexclusion is a literal path segment with no wildcard, so it does not match a bare mirror directory named<name>.git. A bare mirror is archived in full — objects, packs, and refs.classifyGitSourcereports the history shape rather than asking the misleading question "is there a git object database here":bare-repository(history included, not flagged — unless the repository is shallow, which is reported asgit-history-shallowbecause a complete archive of an incomplete object database still cannot restore),work-treeandnested-work-tree(history excluded, flaggedgit-history-excluded),work-tree-pointer(a linked worktree whose.gitis a pointer file; the objects live in the parent repository, so it is flaggedgit-history-externalonly when no other configured source actually archives that object database), andinside-work-tree(a plain subdirectory of someone else's checkout, not flagged — that repository's.gitwas never under the source path, so the exclusion is not what drops its history). A bare repository is identified by layout, includingbare = true, never by a.gitname suffix. The nested scan runs before the ancestor walk, so a source that is both inside a checkout and contains its own nested repository or submodule is still reported. - A work-tree scan that runs out of budget reports
git-history-unknown(andgitHistoryScanTruncatedon the manifest entry), never silence: "not proven clean" is not the same claim as "clean". - Two related coverage traps are reported as
red, because in both the archive contains far less than the tree the source names:source-self-excluded, where the source's own basename matches its exclude list sotar -C <parent> <basename>drops the only top-level member (adding a repository's own.gitdirectory as a source does this, since.gitis excluded by default); andsource-symlink, where a symlinked source archives the link rather than the tree it points at.verifyonly compares checksums, so neither is detectable after the fact. doctorprobes only the external tools the configured destinations actually need. In itstoolsarray,nullmeans "not probed",truemeans "exited 0", andfalsemeans "found, exited non-zero" — a tool absent from$PATHfails the whole command instead, which is tracked as its own fix. See Command Reference.- Source mode
inventory-onlyrecords coverage metadata without archiving file contents. - Runtime contract validation rejects credential-bearing fields and never performs live cloud/database mutations.
- AWS audit commands are read-only. S3 backup, verify, and prune operations perform explicit upload, download, and delete operations against the selected backup destination.
- Retention uses UTC daily, ISO-weekly, and UTC monthly buckets from the configured GFS policy, decided per source rather than per run. Each source gets its own GFS timeline, so a run that archives one source can never evict a different source's only copy. Retention is then applied per run: if any source in a run still needs a copy, the whole run is kept, so a run is never left with manifests describing archives that were deleted. This over-retains multi-source runs on purpose — see
docs/COMMANDS.mdfor why, and for the capacity impact. Zero retention (--daily 0 --weekly 0 --monthly 0) still expires everything; per-source scoping is not a floor,backup holdis. - Prune refuses to expire a preservation-marked source, whatever the policy says. A source whose name or path carries a marker token —
predelete,preservation,delete-prep,do-not-prune, and the rest listed indocs/COMMANDS.md— is pinned with apreserved:<marker>retention reason, and every artifact of that run carriespreservedBy. It reads the naming convention as the intent it already encodes: an archive called<repo>-predeleteexists so that deleting<repo>is safe, and GFS keeping one backup per calendar day per source would otherwise evict it the moment a later run the same UTC day took its daily slot. Matched against the source name, source path, and archive name only, never the destination root or catalog path. Extend per installation withbackup policy set --preserve-markers a,b; the built-ins cannot be removed, and an unusable marker list makes prune refuse to produce a plan rather than produce an unguarded one. This complementsbackup holdand does not replace it: a marker protects archives whose name declares the intent, a hold protects a specific id whatever it is called.
The default state directory is:
~/.hasna/backup/
config.json
manifests/
runs.jsonl
restore-plans/
tmp/
Override it for tests or isolated runs:
HASNA_BACKUP_HOME=/tmp/backup-state backup initThe first internal profile should cover:
backup init
backup sources add ~/.hasna --name hasna-state
backup sources add ~/.codewith --name codewith-state
backup sources add ~/workspace --name workspace
backup destinations add s3 s3://hasna-xyz-infra-backups-prod/open-backup/$(hostname) --name infra-s3 --aws-profile hasna-xyz-infra --region us-east-1
backup run --destination infra-s3
backup verify latestbackup-mcp separates inspection, read-only audit, artifact-only, and mutating tools.
Inspection tools:
backup_statusbackup_inventorybackup_listbackup_verify
Read-only AWS audit tool:
backup_runtime_contract_validatebackup_aws_inventory
Local inspection can initialize the backup home, and verification can materialize temporary archive copies for remote backups.
Artifact-only tool:
backup_restore_planwrites a local restore-plan JSON file and may materialize temporary archive copies for remote backups, but it does not apply a restore.
Mutating tool:
backup_runrequiresallowMutating: trueandconfirmation: "run backup"before it can run.
The MCP surface intentionally does not expose restore apply, prune, or destructive lifecycle changes.
Apache-2.0