Kepr — the library for your Koh repositories.
Complete reference for every operator command, concept, and configuration option.
01 What is Kepr
Kepr is a rolling snapshot library for Koh repositories.
Offer your faces. Let others steal what they need.
Private instance — a rolling snapshot buffer for your own projects. Runs on a machine you own. Keeps the last N saves of every project you offer to it. If something goes wrong, you reach back in. koh offer pushes there automatically.
Public instance — a read-only portal for sharing open source work. You offer explicitly when something is worth sharing. Anyone can browse, steal, and download bundles.
The two instances are completely independent deployments — separate machines, separate databases, separate object stores. They share only the Kepr binary.
02 Quick start
$ curl -fsSL kepr.uk/install.sh | sh
Downloads the Kepr binary, installs to ~/.local/bin, prints next steps.
First run:
$ kepr init
Guided setup. Prompts for domain, data directory, HTTP and SSH ports, and your first operator SSH key. Writes keeper.toml. Generates a recovery phrase — displayed once, store it safely. Generates supervisor integration files.
$ kepr start
Starts the Kepr daemon. Visit your configured domain to confirm.
Connect Koh to your instance:
$ koh steal kepr.local/myproject
Initializes a local Koh repository and sets kepr.local as the default remote. Subsequent koh offer commands push there automatically.
Offer your first save:
$ koh offer
Save in Koh, offer to Kepr. Your snapshot is stored. If you lose your machine, steal it back from anywhere on your network.
03 Core concepts
Faces
A face is a Koh project tracked by Kepr. When you run koh steal kepr.local/peach, peach becomes a face on that Kepr instance. Each face has its own save history, rolling window, and web page.
Offers
An offer is a snapshot pushed from Koh to Kepr. koh offer performs a two-phase protocol — first negotiating which objects Kepr is missing, then sending only those objects. Kepr never receives data it already has.
Steals
A steal is pulling a snapshot from Kepr to a local Koh repository. The result is a fully working Koh repository, objects and all.
The rolling window
Each face maintains a rolling window of max_snapshots saves per lane. When the window fills, the oldest save is pruned. Objects shared between saves are deleted only when the last save referencing them is pruned. max_snapshots = 0 keeps everything.
Objects
Every file is stored as a zlib-compressed blob keyed by its Blake3 hash. Unchanged files between saves are stored once — deduplication is automatic and exact. Files under 512 bytes are inlined in the manifest and never stored as separate objects.
Manifests
Every save is a JSON file in data_dir/manifests/{repo}/{id}.json. The SQLite database is a cache — if lost, kepr recover rebuilds it entirely from manifests. No save is ever lost unless its manifest file is also gone.
Lanes
Koh tracks two lanes: main and dev. Kepr maintains the rolling window separately per lane. A face with max_snapshots = 10 keeps up to 10 main saves and 10 dev saves independently.
04 kepr init
kepr init [--init <system>]
Guided setup. Prompts for domain, data directory, ports, and first operator SSH key. Writes keeper.toml. Generates a recovery phrase shown once. Creates data directory structure. Generates supervisor integration files.
--init <system>: systemd, runit, s6, openrc, launchd, generic, all. Omit to auto-detect.
No kepr init on NixOS — the module handles initialization.
05 kepr start · kepr stop · kepr restart
kepr start kepr stop [--timeout <seconds>] kepr restart
kepr start starts the daemon. Delegates to systemd when available, otherwise starts in the foreground.
kepr stop drains in-flight requests before stopping. Default timeout 30 seconds.
kepr restart drains, stops, starts.
06 kepr status · kepr ping · kepr doctor
kepr status kepr ping kepr doctor
kepr status
kepr · kepr.local · v3.1.0 domain kepr.local uptime 4 days 3 hours repos 14 window 10 saves per repo per lane storage 2.1 GB / 50 GB (4%) memory 180 MB / 8 GB (2%) kepr ● running last offer 2 hours ago last steal 3 days ago
kepr ping
Quick liveness check. Exits 0 if healthy, 4 if unreachable.
$ kepr ping kepr · kepr.local · ok (12ms)
kepr doctor
Full self-diagnostic. SQLite integrity, disk, memory, ports, keys, config, DNS, TLS. Exits 0 all pass, 1 warnings, 2 errors. Safe for monitoring scripts.
07 kepr config
kepr config show kepr config get <key> kepr config set <key> <value> kepr config validate
kepr config set validates before writing and prints whether the change takes effect immediately or requires a restart:
$ kepr config set retention.max_snapshots 20 retention.max_snapshots = 20 takes effect immediately — no restart required $ kepr config set server.ssh_port 2223 server.ssh_port = 2223 restart required: kepr restart
Full configuration reference in §18.
08 kepr keys
kepr keys kepr keys add kepr keys remove <fingerprint>
Operator SSH key management. kepr keys remove refuses to remove the last key — lockout prevention.
Operator keys authenticate koh offer via Ed25519 signed HTTP headers. Key resolution order:
.koh/keeper-key— per-remote key path$KEPR_SSH_KEYenvironment variable~/.ssh/id_ed25519~/.ssh/id_rsa
To use a specific key for a remote:
$ koh steal kepr.example.com/peach --key ~/.ssh/kepr_key
Writes the key path to .koh/keeper-key for subsequent offers.
09 kepr repos
kepr repos kepr repo show <name> kepr repo rename <name> <new> kepr repo remove <name>
kepr repo show full metadata:
$ kepr repo show peach peach main 10 saves window full last offered 2 hours ago dev 3 saves 3 remaining last offered 4 days ago storage 4.2 MB · 847 objects last stolen yesterday created 2026-01-15
kepr repo rename is atomic — all SQLite records and the manifests directory in a single transaction.
kepr repo remove is irreversible. Prompts for confirmation by typing the repo name.
10 kepr prune
kepr prune <name> kepr prune --all kepr prune --dry-run
Prune a repo to its current window immediately. With retention.prune_on_offer = true (default), this runs automatically after every successful offer — manual pruning is rarely needed.
Objects shared between saves are deleted only when the last save referencing them is pruned.
11 kepr offers
Contribution management. Only relevant when contributions.accept_offers = true.
kepr offers pending kepr offer show <id> kepr offer accept <id> kepr offer decline <id> [--reason "<text>"] kepr offer decline --all [--yes]
$ kepr offers pending 3 pending contributions a3f9c2 peach SHA256:AbCd... 2 hours ago REVIEW CAREFULLY b4e891 nina SHA256:EfGh... 1 day ago ACCEPT c5f902 toph SHA256:IjKl... 3 days ago STRONG DECLINE
kepr offer accept moves objects from quarantine to the main store, writes the manifest, runs pruning.
kepr offer decline discards — quarantine objects deleted synchronously. No trace remains.
Full contribution system in §21.
12 kepr backup · kepr restore
kepr backup [--output <path>] kepr restore <backup.tar.gz>
kepr backup creates a tar.gz of SQLite and all objects. Prints output path and SHA-256 on completion.
$ kepr backup wrote kepr-backup-2026-05-09.tar.gz (4.2 GB) sha256: a3f9c2d1...
kepr restore validates integrity before writing. Refuses to run while serving unless --force. Prompts for confirmation by typing the domain name. Creates a pre-restore backup automatically — if restore fails, auto-restores from it.
13 kepr storage
kepr storage status kepr storage gc kepr storage verify [--face <name>] kepr storage trend
kepr storage gc removes orphaned objects — safe to run anytime.
kepr storage verify decompresses and rehashes every object. --face <name> limits to one repo for speed.
kepr storage trend shows 30-day usage chart and disk-full projection.
14 kepr image
kepr image [--output <path>] kepr image --verify <file> kepr restore --from-image <dir>
A Kepr instance image is a complete portable snapshot — data, configuration, and the exact binary that produced it.
$ kepr image wrote kepr-instance-myserver-2026-05-09.tar.gz (4.1 GB) checksum: blake3:a3f9c2... 14 repos · 247 saves · 12,847 objects
Restore on a new machine:
$ tar -xzf kepr-instance-myserver-2026-05-09.tar.gz $ cd kepr-instance-myserver-2026-05-09/ $ ./kepr restore --from-image . --init systemd
The binary that produced the image is included. The README.txt has plain-text restoration instructions — readable without any tools, on any machine, in any future.
Schedule automatically:
[images] schedule = "monthly" keep = 3
Full detail in §23.
15 kepr recover
kepr recover [--verify] [--dry-run]
Rebuilds SQLite entirely from the manifest JSON files in data_dir/manifests/. Every repo, save, tag, and object reference is reconstructed. No save is lost unless its manifest is also gone.
--verify checks all objects against their hashes after rebuilding.
Handles dangling parent_id references gracefully — stolen saves may reference parents that were never transferred.
16 kepr logs
kepr logs [--since <duration>] [--service <name>]
Tails the structured JSON log as human-readable lines.
14:23:01 offer peach a3f9c2 wanshitong@library 14:18:44 steal peach a3f9c2 anonymous 13:55:12 offer nina b4e891 wanshitong@library
--since: 1h, 30m, 24h. --service caddy on journald systems.
17 Koh integration
koh steal <kepr-url>[@<tag|id>] koh offer [<kepr-url>] [--local] koh offers koh apply <id>
koh steal
Initialize a local Koh repository from a Kepr snapshot. Stores the Kepr URL in .koh/keeper as the default remote.
$ koh steal kepr.local/peach $ koh steal kepr.local/peach@v1.0.0 # steal tagged save $ koh steal kepr.local/peach@a3f9c2 # steal specific save
If the pinned tag is flagged, a warning is shown before any files are written.
koh offer
Push the current save to the default Kepr remote.
$ koh offer $ koh offer kepr.example.com/peach # specific remote $ koh offer --local # write .face to disk
--local writes a self-contained .face file — a portable bundle of the manifest and all objects, applicable on any machine.
koh offers
List recent offers in the interactive pager.
koh apply
Apply a specific save by ID or from a local .face file.
$ koh apply a3f9c2 $ koh apply ./peach-a3f9c2.face
18 Configuration reference
| Key | Type | Default | Restart |
|---|---|---|---|
| server.domain | string | — | yes |
| server.http_port | int | 8080 | yes |
| server.http_bind | string | 0.0.0.0 | yes |
| server.ssh_port | int | 2222 | yes |
| server.ssh_bind | string | 0.0.0.0 | yes |
| storage.data_dir | path | /var/lib/kepr | yes |
| instance.public | bool | false | yes |
| retention.max_snapshots | int | 10 | no |
| retention.prune_on_offer | bool | true | no |
| retention.max_bundle_age_days | int | 0 | no |
| contributions.accept_offers | bool | false | no |
| contributions.require_review | bool | true | no |
| contributions.require_auth | bool | true | no |
| contributions.auto_activate | bool | true | no |
| contributions.welcome_message | string | — | no |
| review.agent_enabled | bool | false | no |
| review.inference_url | string | — | no |
| review.inference_model | string | — | no |
| review.agent_timeout_s | int | 180 | no |
| images.dir | path | {data_dir}/images | no |
| images.keep | int | 3 | no |
| images.schedule | string | "never" | no |
| ops.tailscale_only | bool | false | no |
Private instance
[server] domain = "kepr.local" http_port = 8080 ssh_port = 2222 [storage] data_dir = "/var/lib/kepr" [instance] public = false [retention] max_snapshots = 10 prune_on_offer = true [images] schedule = "monthly" keep = 3
Public instance
[server] domain = "kepr.example.com" http_port = 8080 ssh_port = 2222 [storage] data_dir = "/var/lib/kepr" [instance] public = true [retention] max_snapshots = 0 prune_on_offer = false [contributions] accept_offers = true require_review = true welcome_message = """ Your welcome message here. """ [review] agent_enabled = true inference_url = "http://llm.local:8080/v1/chat/completions" inference_model = "your-model-name"
19 Data directory
data_dir/
kepr.db SQLite — cache, always rebuildable
kepr.db-wal
kepr.db-shm
kepr.sock Unix control socket
kepr.pid PID file
kepr.prev Previous binary (kept by upgrade)
manifests/ SOURCE OF TRUTH
{repo}/
{save_id}.json Verbatim ManifestJson
objects/ Content-addressed store
{hh}/
{remaining62hex} zlib-compressed, Blake3-keyed
images/ Instance images
quarantine/ Pending contribution objects
logs/
kepr.log Structured JSON log
manifests/ is the source of truth. If everything else is lost, kepr recover rebuilds from manifests alone.
20 Init system adapters
Kepr is init-system-agnostic. It handles its own signals and writes its own PID file.
kepr init --init <system> generates:
| System | Files |
|---|---|
| systemd | kepr.service |
| runit | run, finish |
| s6 | run, finish, notification-fd |
| openrc | kepr |
| launchd | software.asha.kepr.plist |
| generic | start.sh, README.txt |
Generic adapter — works with any POSIX supervisor:
#!/bin/sh exec /usr/local/bin/kepr daemon \ --config /etc/kepr/keeper.toml
Signal contract:
| Signal | Behavior |
|---|---|
| SIGTERM | Graceful drain, exit 0 |
| SIGINT | Same as SIGTERM |
| SIGHUP | Reload non-restart-required config |
| SIGUSR1 | Reopen log file |
| SIGUSR2 | Trigger storage GC |
21 Contributions
Allow others to offer to your public instance. Their offers go to a review queue before joining the library.
Enable
[contributions] accept_offers = true require_review = true
Contributor signup
Contributors visit /contribute, paste their SSH public key, and are registered immediately. No username, no email, no password. Their fingerprint is their identity.
After registering:
$ koh offer kepr.uk/koh
The offer goes to your review queue.
Review
$ kepr offers pending $ kepr offer show a3f9c2 $ kepr offer accept a3f9c2 $ kepr offer decline a3f9c2 --reason "out of scope"
Or via the ops web UI at /ops/review.
Contributor key management
kepr contrib list kepr contrib revoke <fp> kepr contrib block <fp>
Per-repo settings
[contributions.repos.koh] open = true [contributions.repos.zig-ssh] open = false
22 Agentic review
Kepr pre-reviews contributions using a local language model. The assessment waits for you when you open the review page. Never blocking. Always optional.
Setup
[review] agent_enabled = true inference_url = "http://llm.local:8080/v1/chat/completions" inference_model = "your-model-name" agent_timeout_s = 180
Any local inference server with a chat completions API works. Your code never leaves your network.
Assessment format
VERDICT: REVIEW CAREFULLY CONFIDENCE: HIGH SUMMARY: ... WHAT CHANGED: ... CONCERNS: ... WHAT LOOKS GOOD: ... RECOMMENDATION: ...
What the agent cannot do
The agent cannot accept or decline. You always decide. The accept button requires viewing every changed file and waiting 60 seconds regardless of the agent's verdict.
When the inference endpoint is unavailable, the review page works fully without the assessment.
23 Instance images
A complete portable snapshot — data, configuration, and the exact binary that produced it. See kepr image in §14.
The content checksum is Blake3 over the sorted concatenation of every manifest and object. Verifiable with any Blake3 implementation, independently of Kepr.
The README.txt in every image has plain-text restoration instructions. Plain ASCII. Readable on any terminal, in any future, without any tools.
Schedule in config:
[images] schedule = "monthly" keep = 3
24 Ops web UI
The operator interface lives at /ops. Always protected — either by Tailscale IP restriction (ops.tailscale_only = true) or by username and password (default).
Set credentials at kepr init. Manage:
kepr ops password kepr ops sessions kepr ops sessions revoke --all
Pages
| Route | Purpose |
|---|---|
| /ops | Dashboard |
| /ops/repos | Repo management |
| /ops/repos/:name | Repo detail |
| /ops/storage | Storage and trend |
| /ops/keys | Key management |
| /ops/config | Visual config editor |
| /ops/log | Live log tail |
| /ops/review | Contribution queue |
| /ops/recover | Recovery wizard |
The attention section surfaces anything needing action. Empty means all clear.
25 NixOS module
# private backup instance
services.kepr = {
enable = true;
domain = "kepr.local";
tailscale = true;
public = false;
maxSnapshots = 10;
authorizedKeys = [ "ssh-ed25519 AAAA... wanshitong@library" ];
};
# public portal instance
services.kepr = {
enable = true;
domain = "kepr.example.com";
tailscale = false;
public = true;
maxSnapshots = 0;
authorizedKeys = [ "ssh-ed25519 AAAA... wanshitong@library" ];
};
The module handles systemd units, users, directories, firewall, Caddy virtual host, and Tailscale IP binding.
authorizedKeys is the source of truth for operator keys — adding or removing a key and running nixos-rebuild switch takes effect immediately.
Contributor keys (web UI registered) are stored separately — nixos-rebuild switch never touches them.
Scheduled images:
services.kepr.images = {
schedule = "monthly";
keep = 3;
};
26 Signal handling
| Signal | Behavior |
|---|---|
| SIGTERM | Graceful drain, exit 0 |
| SIGINT | Same as SIGTERM |
| SIGHUP | Reload non-restart-required config keys |
| SIGUSR1 | Reopen log file |
| SIGUSR2 | Trigger storage GC |
SIGHUP applies immediately: retention.*, contributions.*, review.*, images.*.
Keys requiring restart (server.*, storage.data_dir, instance.public) are logged as warnings on SIGHUP but not applied.
27 Upgrading
kepr upgrade [--version <x.y.z>] [--dry-run] [--rollback]
Verifies SHA-256 and Minisign signature before touching anything. Takes a backup if none exists in the last 24 hours. Swaps binary, restarts, verifies health. Auto-rollbacks if health check fails within 30 seconds.
Previous binary kept at data_dir/kepr.prev for one version. --rollback restores it manually.