Standalone timestamped backups — files, folders, symlinks, hardlinks
stach is a small, dependency-free CLI for fast local backups. Point it at files or directories and get timestamped copies: plain files are duplicated beside the original; directories become a single .tar.zst (ustar + zstd) that preserves symlinks and hardlinks. Unpack with stach -x or tar + zstd when those tools are available.
Written in Odin. Compression uses embedded zstd (level 1, multi-threaded). No system zstd required at runtime.
- Timestamped file copies —
notes.txt→notes.txt.20260714120000(likecp -p) - Directory archives —
project/→project.20260714120000.tar.zst - List mode — no paths → pack
<dirname>.listentries into one<dirname>.<timestamp>.tar.zst(-ffor a custom list) - Symlinks & hardlinks — stored correctly in the tar stream
- Extract mode —
stach -xrestores files, dirs, and links safely - Parallel entities —
-jprocesses multiple top-level paths at once - Parallel compress & extract —
-cmulti-frame zstd (parallel compress + decompress) and parallel file writers - Folder pack progress — live single-line progress while building archives
- Quiet mode —
--quietsuppresses non-error output - Multi-arch releases — Linux amd64/arm64, macOS Apple Silicon, Windows amd64
Download from Releases, then:
chmod +x stach-linux-amd64 # or the asset for your OS
./stach-linux-amd64 notes.txt src/Requires Odin and a C compiler (cc / MSVC on Windows).
git clone https://github.com/macedot/stach.git
cd stach
make
./stachstach [--quiet] [-j N] [-c N] <file|directory|pattern> ...
stach [--quiet] [-c N] [-f list_file]
stach [-c N] -x <archive.tar.zst> [dest_dir]
| Mode | Behavior |
|---|---|
| File | Copy to <path>.<timestamp> |
| Directory | Pack to <path>.<timestamp>.tar.zst (ustar + zstd level 1) |
| List | No paths → if <cwd-basename>.list exists, pack all its entries into one <cwd-basename>.<timestamp>.tar.zst |
-f FILE |
Same as list mode using a custom list file |
-x |
Unpack .tar.zst into dest_dir (default: .) |
| Flag | Default | Description |
|---|---|---|
-j N |
CPU count | Max paths to process in parallel |
-c N |
CPU count | multi-frame zstd workers (pack + extract decompress) and extract file writers |
-f FILE |
<cwd-basename>.list when present |
List file: one path per line; # comments; wildcards; missing entries warn and skip |
--quiet |
off | Suppress all non-error output (progress + src -> dst) |
No arguments and no default list file prints usage and exits with code 2. Exit 1 if any path fails.
In default mode, stdout shows live single-line folder packing progress plus mapping lines:
src -> dst
Use --quiet to keep stdout empty unless an error occurs (errors still go to stderr).
# Backup a file and a tree
./stach notes.txt src/
# Parallel jobs + zstd workers
./stach -j 4 -c 8 data/*.csv project/
# Shell globs (or let stach expand patterns)
./stach 'logs/*.log'
# List mode: pack every entry of <dirname>.list into one archive
# Example project.list:
# .config
# .ssh
# Documents/*.pdf
./stach
./stach -f backup.list
# Extract (parallel file writes via -c)
./stach -c 8 -x project.20260714120000.tar.zst restored/
# Compatible with system tar + zstd when installed
zstd -d -c project.20260714120000.tar.zst | tar -tf -
zstd -d -c project.20260714120000.tar.zst | tar -xf - -C restored/| Input | Output |
|---|---|
| Regular file | Byte copy next to the source, suffix .<YYYYMMDDHHMMSS> |
| Directory | Walk with lstat → ustar members (0 file, 5 dir, 2 symlink, 1 hardlink) → multi-frame zstd → .tar.zst |
| List file | Resolve lines (comments/globs/missing skips) → multi-root walk into one .tar.zst named after the current directory |
Hardlinks share an inode: the first copy stores data; later names reference it. Symlinks store the link target, not the pointed-to content. In list mode, hardlink dedup is shared across all list roots.
Packing splits the tar into independent zstd frames (when -c > 1 and the tree is large enough) so both compress and decompress scale across cores. Extract inflates frames in parallel, creates directories, writes regular files in parallel, then applies symlinks/hardlinks. Path traversal (.., absolute names) is rejected before writing under the destination.
make testCovers file copy, nested tree, symlink, hardlink, stach -x, and list mode (*.list / -f).
benchmark/bench.sh # WORKERS=N RUNS=N to override defaultsCompares archive formats (zstd, lz4, gzip, xz, brotli, store) against stach's embedded zstd: pack/extract times and ratio on a generated mixed corpus, with a decompress | tar -x round-trip gate per candidate. Latest numbers in benchmark/RESULTS.md. Requires zstd, lz4, xz, brotli, python3.
Publishing a GitHub Release runs .github/workflows/release.yml and attaches:
| Asset | Platform |
|---|---|
stach-linux-amd64.tar.gz |
Linux x86_64 |
stach-linux-arm64.tar.gz |
Linux ARM64 |
stach-darwin-arm64.tar.gz |
macOS Apple Silicon |
stach-windows-amd64.zip |
Windows x86_64 |
| Path | Role |
|---|---|
src/ |
Odin sources (CLI, list mode, tar write/read, copy) |
vendor/zstd/ |
Embedded zstd library |
vendor/stach_zstd_wrap.c |
Thin C API used from Odin |
build/ |
Local objects / static lib (not tracked) |
.github/workflows/ |
Multi-arch release builds |
stach is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
Bundled zstd retains its own licenses (see vendor/zstd/LICENSE and vendor/zstd/COPYING).