On this page
saferm is an AI-first safe rm replacement that archives files instead of permanently deleting them, with rich metadata capture and full undo support.
#saferm
AI-first safe rm replacement. Archives files instead of deleting them.
#CLI Reference
#Packages
saferm is organized into internal packages that handle distinct responsibilities: file and directory archival with integrity verification, SQLite-based metadata storage, git context detection, and environment and process metadata capture. Each package is independently testable and designed for concurrent use via WAL-mode SQLite and atomic file operations. Configuration is handled by strictcli's built-in config system (TOML format at ~/.saferm/config.toml).
#internal/archive
Package archive handles file and directory archival to the saferm archive. It supports atomic same-filesystem renames, cross-device copy-and-verify, and tar+zstd compression for directories.
#KindFile
const KindFile Kind = iotaThe three shapes an archived entry takes on disk.
#KindDirectory
const KindDirectory#KindSymlink
const KindSymlink#ErrFileNotFound
var ErrFileNotFound = errors.New("file not found")Sentinel errors.
#ErrRecursiveRequired
var ErrRecursiveRequired = errors.New("target is a directory; recursive flag required")#ErrConflict
var ErrConflict = errors.New("destination already exists")#ErrHashMismatch
var ErrHashMismatch = errors.New("hash mismatch after copy")#ArchiveResult
type ArchiveResult structArchiveResult holds the outcome of archiving a file or directory.
#Kind
type Kind intKind names what an archival is about to move.
#Plan
type Plan structPlan is everything an archival can determine by reading: what the entry is, where it will land, and (for a symlink) what it points at. Building one mutates nothing, so a caller can render a plan as a preview and stop, or hand it to [Execute] and go through with it.
#NewPlan
func NewPlan(path string, archiveDir string, isRecursive bool) (*Plan, error)NewPlan inspects path and resolves where archiving it would put it. It performs no mutation.
#Execute
func Execute(p *Plan) (*ArchiveResult, error)Execute carries out the archival a [Plan] describes.
#Archive
func Archive(path string, archiveDir string, isRecursive bool) (*ArchiveResult, error)Archive moves a file or directory into archiveDir, returning the result. For files: moved directly (or copied cross-device) with SHA-256 hash. For directories: compressed into a .tar.zst archive.
#Restore
func Restore(uuid string, archiveDir string, destPath string, isDirectory bool, force bool, symlinkTarget string) errorRestore extracts an archived file or directory to destPath. When symlinkTarget is non-empty, the entry is restored as a symlink pointing to that target (no physical archive file is read).
#internal/db
Package db manages the SQLite database tracking all saferm deletions. It uses WAL mode and busy_timeout for concurrency safety across multiple simultaneous sessions.
#SchemaSQL
const SchemaSQL = `#ErrNotFound
var ErrNotFound = errors.New("record not found")ErrNotFound is returned when a queried record does not exist.
#DB
type DB structDB wraps a *sql.DB connection to the saferm SQLite database.
#DeletionRecord
type DeletionRecord structDeletionRecord represents a single archived deletion in the database.
#Open
func Open(dbPath string) (*DB, error)Open opens (or creates) the SQLite database at dbPath with WAL mode and busy_timeout=5000ms, then runs the schema DDL.
#DB.Close
func (d *DB) Close() errorClose closes the underlying database connection.
#DB.Insert
func (d *DB) Insert(rec *DeletionRecord) (int64, error)Insert inserts a DeletionRecord and returns the auto-increment ID.
#DB.QueryByID
func (d *DB) QueryByID(id int64) (*DeletionRecord, error)QueryByID retrieves a single record by ID. Returns ErrNotFound if it does not exist.
#DB.QueryByPath
func (d *DB) QueryByPath(path string) ([]*DeletionRecord, error)QueryByPath returns all non-restored records matching the given original_path, ordered by deleted_at DESC (newest first).
#DB.QueryAll
func (d *DB) QueryAll(includeAll bool) ([]*DeletionRecord, error)QueryAll returns all records ordered by deleted_at DESC. If includeAll is false, restored and purged records are excluded.
#DB.MarkRestored
func (d *DB) MarkRestored(id int64, restoredTo string) errorMarkRestored sets restored_at to now and restored_to to the given path. Returns ErrNotFound if the record does not exist.
#DB.MarkPurged
func (d *DB) MarkPurged(id int64) errorMarkPurged sets purged_at to now, preserving the metadata record. Returns ErrNotFound if the record does not exist.
#DB.QueryOlderThan
func (d *DB) QueryOlderThan(before time.Time) ([]*DeletionRecord, error)QueryOlderThan returns all non-restored, non-purged records deleted before the given time.
#internal/git
Package git provides helpers for managing the git index alongside saferm's archive/restore operations.
#IsInGitRepo
func IsInGitRepo(dir string) boolIsInGitRepo returns true if dir is inside a git working tree.
#IsGitTracked
func IsGitTracked(path string) boolIsGitTracked returns true if the file at path is tracked by git (i.e., known to the index). The path must be absolute or relative to the current working directory; the command runs from the file's parent directory.
#GitRmCached
func GitRmCached(path string, recursive bool) errorGitRmCached stages the removal of path in the git index without touching the working tree (the file is already archived). When recursive is true, -r is added for directory removal.
#GitAdd
func GitAdd(path string) errorGitAdd stages a file in the git index. The command runs from the file's parent directory.
#internal/meta
Package meta collects rich metadata about each deletion: environment variables, git repository context, parent process information, and arbitrary user-supplied key-value pairs.
#Metadata
type Metadata structMetadata holds contextual information captured at deletion time.
#Collect
func Collect(excludePatterns []string, customMeta map[string]string) (*Metadata, error)Collect gathers metadata from the current environment.
Best-effort per collector -- git context and parent-process details degrade to empty values rather than failing. The one thing it refuses to work around is an exclude pattern that does not compile: that is the caller asking for a redaction, and continuing without it would write the very variables the caller meant to keep out. See collectEnv.