Koh — 45 commands.
Complete reference for every primary command, concept, and configuration option.
01 Quick start
shell (macOS / Linux)
curl -fsSL https://kepr.uk/koh/install.sh | sh → downloads the prebuilt binary, verifies Blake3 checksum; builds from source when no binary is published. Lands in ~/.local/bin (override with KOH_INSTALL_DIR).
nix (add registry once to ~/.config/nix/nix.conf)
echo 'extra-flake-registry = https://kepr.uk/nix-registry.json' >> ~/.config/nix/nix.conf then:
nix profile add koh on NixOS, use nix.settings.extra-flake-registry in configuration.nix and rebuild, or programs.koh.enable = true; for the module.
- 01
koh initInitialize a repository. Creates
.koh/with a SQLite database and default.kohconfig. Pass--configto open the config file immediately. - 02
koh save "message"Snapshot the working tree. Transactional — a failure at any stage leaves no partial state.
- 03
koh logBrowse history in the interactive pager. Tab cycles filter modes (all → green → red). Piped output disables interaction and color automatically.
koh logsis an alias for the same command. - 04
koh promotePromote dev HEAD to main. Requires confirmation. Undo with
koh lane main && koh undo.
.koh/ to your .gitignore. Koh and Git are separate systems that can share the same working tree without overlapping or disrupting each other, so you can try Koh inside your current setup without danger and decide later whether the workflow fits.
02 Core concepts
Saves
A complete, immutable snapshot of your working tree — identified by the first six hex characters of its Blake3 content hash. Written once, never modified. History is append-only.
Two Lanes
Exactly two: main (stable, what works) and dev (experimental, what you're trying). No arbitrary branches, no merges, no rebase. Promote dev in one atomic operation when ready.
Content-Addressed Storage
Every file is identified by its Blake3 hash. Unchanged files between saves are stored once. Files under 512 bytes are inlined in the manifest; larger files live zlib-compressed in .koh/objects/.
Checkpoints
Saves with is_checkpoint = 1 — deliberate named safety points. Set one before handing off to an agent or starting a risky refactor. koh load --checkpoint finds the most recent one without needing an ID.
The SQLite database is a query cache, not the primary store. All structural data lives in manifest JSON files at .koh/snapshots/. If the database is lost, koh recover rebuilds it from manifests — no save is ever lost unless its manifest file is also gone.
03 Core commands
Initialize a new repository. Creates .koh/, a fresh SQLite database, and a default .kohconfig. With --config, opens .kohconfig in $EDITOR (then $VISUAL, then vi) immediately after initialization.
Snapshot the working tree. The save is transactional — manifest written to .tmp.json, database INSERT and HEAD update in a single SQLite transaction, then atomically renamed. A failure at any stage leaves no partial state.
--agent records the AI model that initiated the save. --session groups saves under a named session for filtering and undo. --json suppresses interactive output and emits a single JSON object — designed for agent consumption.
Show save history. Opens in the interactive pager on a TTY; prints unfiltered to stdout when piped. --all interleaves both lanes by timestamp. --session filters to saves with the specified session name. --json outputs all matching saves as a JSON array with no pagination.
--since accepts: yesterday, today, last week, N days ago, last Monday, YYYY-MM-DD, or YYYY-MM-DDThh:mm.
Alias for koh log.
Filtered view of koh log showing only non-checkpoint saves (is_checkpoint = 0). Accepts the same flags as koh log.
Filtered view of koh log showing only checkpoint saves (is_checkpoint = 1). Useful for reviewing declared safety points or finding a checkpoint ID before koh load --checkpoint. Accepts the same flags as koh log.
Show working-tree changes versus the current HEAD. Lists new files (+), modified files (~), and deleted files (-). --json outputs a JSON object with lane, head_id, dirty, modified, added, and deleted fields — no ANSI codes, no pagination.
Produce a unified diff versus the current HEAD, a specific save, or two explicit saves. Pipes colorized output through $PAGER on a TTY; prints directly when piped. --json outputs a JSON object with a files array and a diff field.
Diffs between the earliest and latest saves of the most recent named session on the current lane. last is a keyword that resolves to the most recent session name.
04 Lane commands
Without arguments, print the current lane. With main or dev, switch to that lane. Lane switching warns if there are unsaved changes but does not block — switching lanes carries no risk of data loss.
Promote dev HEAD to main. With <id>, promote a specific ancestor of dev HEAD rather than the tip — essential when your dev tip has work-in-progress above stable saves you want to ship. Validates that the target is within dev's ancestry chain and refuses orphaned saves. The output shows the undo hint: koh lane main && koh undo.
Apply one save's delta to the current HEAD as a new save. Koh performs a three-way merge against the source save's parent; clean applies modify, add, or remove exactly that delta, while conflicts abort. --dry-run previews without touching the working tree.
Show a diff of dev HEAD versus main HEAD — everything that has accumulated on dev. Output is piped through the pager.
05 Undo, Load, Revert, Peek
These commands alter working-tree state or history. All require explicit confirmation. All require a clean working tree — no unsaved changes — before executing.
Rewind the current lane by n saves (default 1). Stores the pre-undo HEAD in the database (_pre_undo_main or _pre_undo_dev) so the operation can be manually reversed. Skipped saves are not deleted — they remain in the object store. When the undo would cross a checkpoint, shows which checkpoints would be undone before confirming. --yes skips the prompt.
Undo all saves belonging to the most recent named session on the current lane in a single operation. Shows the session name and count before confirmation. Session saves remain in the saves table — they are accessible by ID.
Restore the working tree to any save on the current lane. Validates same-lane membership before any other checks. Requires confirmation.
Load the most recent checkpoint on the current lane without needing its ID. Shows the checkpoint message and timestamp before confirmation. Exits with an error if no checkpoints exist on the current lane.
Throw away all working-tree changes and restore to the current HEAD. Requires confirmation. No history is altered.
Non-destructively extract a save to .koh/peek-<id>/ for inspection. Nothing in the working tree is touched. Requires confirmation. The peek directory is temporary and cleaned up by subsequent operations.
06 Checkpoints
A checkpoint is a save with is_checkpoint = 1 — a deliberately named recovery point. The intended use: set one before handing off to an AI agent, before a risky refactor, or any time you want to be able to say "take me back to here" without remembering a save ID.
koh load --checkpoint always returns to the most recent checkpoint on the current lane automatically.
Create a checkpoint save on the current lane. The message is required — checkpoints must be intentional and described. Follows the same atomicity pattern as koh save. Checkpoints appear in koh log with a filled ✦ marker in amber. Unlike koh save, this command does not refuse on a dirty working tree — the entire point is to snapshot exactly where you are, including in-progress changes. Warns on a vague message (containing words like "checkpoint", "wip", "before", or "save") or an immediate-repeat checkpoint; --yes bypasses both warnings.
07 Cleanup & Recovery
Update the message on a save. The only command that mutates a manifest — specifically, only the message field via atomic re-serialization. Checksum and all other fields are unaffected.
Identify and delete orphaned saves — saves not reachable from either HEAD by following parent pointers. A recursive SQL CTE identifies the full reachable set; anything outside it is eligible for deletion. Requires confirmation.
Rebuild the SQLite database from manifest files. When the database already holds saves, confirms before wiping and rebuilding — --yes skips that prompt for scripted use. With --verify, validates all manifest checksums and object integrity before rebuilding. Reports a "N saves · M checkpoints · K flags" summary after rebuild and suggests koh verify for a deeper integrity check. Run after any database corruption, accidental deletion, or schema mismatch.
Read-only integrity check. Recomputes the Blake3 checksum of every manifest and every stored object, reports any mismatch, and exits non-zero if anything is invalid. Opens results in the interactive pager.
Permanently remove a file or directory from save history. Pass a file path or directory prefix and Koh rewrites every affected manifest to exclude it, then prunes any objects no longer referenced by any remaining manifest. Without --save, rewrites the entire history; with --save <id>, rewrites only that one save (prefix match accepted).
Before executing, Koh shows a summary of which saves are affected and which paths will be removed, then asks for confirmation. --yes skips the prompt for scripted use. Save IDs and parent chains are preserved — only the file list and content checksum change, so koh verify passes cleanly after stripping.
koh checkpoint before running strip. Use this command when sensitive content — credentials, internal documents, large binaries — was accidentally committed and needs to be removed from all history, not just HEAD.
08 Flags
Mark a save as unsafe. Save-level flag metadata is written into the manifest and database. The reason is optional, but when present it is shown anywhere the save is surfaced.
Clear a save-level flag.
List all flagged saves in the current repository.
09 Health
Read-only diagnostic command. Never modifies anything. Always exits 0 — health output is informational, never a gate. Signals are only shown when their threshold is exceeded; a clean repo shows all signals clean.
Analyse save history and report behavioural signals. Six signals, all with configurable thresholds in .kohconfig:
| Signal | What it flags |
|---|---|
| Frequent Files | Files appearing in an unusually high proportion of saves — may belong in .kohignore. Threshold: health_frequent_file_pct (default 80%) |
| Large Saves | Saves touching an unusually large number of files. Threshold: health_large_save_files (default 20 files) |
| Undo Patterns | Files that are frequently reverted — may indicate unstable decisions. Threshold: health_undo_threshold (default 3) |
| Dev Lane Age | Dev lane not promoted to main in an unusually long time. Threshold: health_dev_age_days (default 30 days) |
| Save Velocity | Unusually low rate of saves over recent time. Threshold: health_min_saves_per_week (default 3) |
| Storage Growth | Month-over-month storage growth exceeds threshold. Threshold: health_storage_growth_pct (default 200%) |
10 Meta
Dashboard view of Koh projects. By default it reads ~/.config/koh/projects, where repositories are auto-registered when commands run. With --path, scans a specific directory instead.
Check for or install the latest Koh release. --check reports availability without downloading.
Open .kohconfig in $EDITOR (then $VISUAL, then vi). Creates the file with defaults if it doesn't exist. Exits with an error if run outside a Koh repository.
Display the embedded man page in the interactive pager. Section headers are rendered in amber, command names highlighted. Tab and Shift+Tab navigate between section headers (not filter modes). The status bar shows the current section name.
Print the version string.
Print the usage summary.
Delete a single face from the connected Kepr instance. Requires confirmation. The face is removed from Kepr — local saves and manifests are unaffected.
Delete an entire repository from the connected Kepr instance. Requires double confirmation by typing the repository name. Irreversible on the remote — local history is untouched.
Environment Variables
| Variable | Effect |
|---|---|
| KOH_DEBUG | Print the full error type on failure — useful for bug reports |
| NO_COLOR | Disable all ANSI color output |
| KOH_NO_COLOR | Same as NO_COLOR |
| EDITOR | Editor used by koh config and koh init --config |
| VISUAL | Fallback editor if EDITOR is unset (checked before vi) |
11 Agent Workflow
Koh has first-class support for AI agent workflows. The additions are minimal and composable — they do not change core save semantics, they add metadata and filtering that makes agent-produced history legible and reversible.
Save Metadata
koh save --agent "model-name" --session "session-name" records which AI model produced a save and groups it with other saves in the same logical session. Both fields are optional and stored in the saves table. When both are present, koh log shows them as a [agent · session] suffix.
Session Undo
koh undo --session rolls back all saves in the most recent named session in a single confirmation. The intended recovery path when an agent session produces unwanted results. Session saves are not deleted — they remain accessible by ID.
Checkpoints
Before handing off to an agent: koh remote status, then koh checkpoint "before agent session". After the session, if anything went wrong: koh revert then koh load --checkpoint. No ID required.
JSON Output
Four commands accept --json for machine-readable output with no ANSI codes and no pagination.
| Command | JSON fields |
|---|---|
| koh save --json | id, lane, message, timestamp, size_bytes, agent, session |
| koh status --json | lane, head_id, dirty, modified, added, deleted |
| koh log --json | Array of save objects with all fields including flagged, flag_reason, agent, session, is_checkpoint |
| koh diff --json | files array, diff unified diff string |
Strict Koh Discipline
The Philosophy
The discipline exists because without it, the history is present but not legible. The safety net is there, but its anchor points are unnamed.
Before Anything Else
Three commands. Every time. No exceptions.
dirty: true means stop. Someone left unsaved work. Do not save over it.
Checkpoint Before Everything
A checkpoint is a named safety marker at the current HEAD. It is not a save - it marks a moment you can return to without remembering an ID.
Set one before any work session, risky operation, or schema change.
Sessions & Attribution
Choose a specific session name before the first save, then use the exact same name on every save in the session.
Session names and --agent make rollback and auditing possible.
The Discipline Loop
Do the work, check what changed, then save one coherent outcome.
Run tests before saving. If one sentence needs an and to explain the change, split it into two saves.
Save Messages
Messages should name the outcome, not the attempt.
Bad saves are vague. Good saves stand on their own months later.
The Log Glyphs
If a save becomes known-bad, flag it immediately with koh flag <save-id>.
Undo and Recovery
koh undo --session rolls back the latest named session. koh load --checkpoint returns to the latest checkpoint.
Use undo for unwanted sessions. Use load when the work is too tangled to salvage incrementally.
Forbidden Actions
koh promotekoh revertkoh cleankoh recoverkoh offer --publicWhy This Discipline Matters
Strict Koh Discipline keeps the history legible: undo becomes reliable, checkpoints become real safety points, and saves stay attributable. Without it, the safety net still exists, but its anchor points are unnamed.
12 LOG.md
LOG.md is the rolling 7-day summary Koh maintains automatically at the repository root. It is regenerated whenever Koh writes history, and it is the first file agents should read before starting work.
The full audit trail lives in .koh/log/, nested by year, month, ISO week, and day. Every command is appended to that day's activity log, and history-writing events are mirrored into the narrative history. The daily files accumulate forever, while the root LOG.md stays small, is excluded from snapshots unconditionally, and can be recreated from the database during recovery.
13 Interactive Pager
Commands that produce multi-line output use the interactive pager on a TTY. On non-TTY output (piped or redirected), all lines are printed directly with no interaction and no color.
Commands using the interactive pager: log, overview, verify, offers, saves, checkpoints, issues, issue, clean, man.
Commands that pipe to $PAGER: diff, diverge, pluck --dry-run.
Filter Modes (history commands)
| Mode | Shows |
|---|---|
| all | Every line |
| green | Saves older than stable_days |
| red | Flagged saves |
Tab cycles forward. Shift+Tab cycles backward. Green = stable or noteworthy. Red = needs attention.
Section Navigation (koh man)
In the man pager, Tab and Shift+Tab navigate between section headers rather than cycling filter modes. A section header is a line that is all-caps and not indented. The status bar shows the current section name. All lines are always visible — there are no filter states.
14 Kepr Integration
Kepr is an optional remote service for sharing and syncing Koh repositories. Local use requires no Kepr instance. Koh supports multiple named remotes — private instances for backup, public instances for sharing — each with explicit visibility and independent confirmation behavior.
The Remote System
Koh connects to Kepr instances through named remotes configured in .koh/remotes. There is no default instance. The remote URL comes from the user - always.
Offering Saves
koh offer is private and silent by default. Public offers show a confirmation with the save message, changed files, size, and visibility.
Public offers always show a confirmation before proceeding. That friction is intentional.
Remote Configuration
Remote configuration lives in .koh/remotes. Each remote has a name, URL, visibility (private or public), and optional default and handle= flags.
Show all configured remotes with live status — whether each remote is current, how many saves behind it is, and how many offers are pending.
Add a remote. Probes the instance for reachability. On public instances, offers to claim a handle via the existing SSH flow. Prompts for visibility if the flag is omitted.
Change which remote koh offer targets when no name is given.
Manage remotes. handle checks or claims a handle on a public remote.
Offering
Offer the current save to the default remote. If the default remote is private, offers silently with no confirmation. A live progress line shows the save currently uploading, so a slow or stalled transfer is visible rather than a frozen counter. When the target repo does not exist on the remote, Koh automatically runs the setup wizard to create it — no extra flags needed.
Size guard. Before any upload, Koh refuses a save containing a file over 100 MiB, or a total over 500 MiB, naming the offending file so you can remove it. The Kepr server enforces the same limits as a hard boundary — oversized offers are rejected at negotiation, before a single byte is transferred.
Project identity. Every repository carries a project_id generated at koh init and embedded in each save. Kepr records it on first offer and rejects any save from a different project chain — so a misconfigured remote can never land one project's saves in another repository.
Offer to one or more named remotes. Public remotes always show a confirmation prompt with the save message, files changed, and a statement that the save will be publicly visible.
Select which remotes to offer to: every private remote (silent) or every public remote (one combined confirmation). Pair with --all to send the full history to the whole set.
These flags preset the setup wizard fields when the remote repo does not exist yet. --builds enables Kepr Builds non-interactively (writes kepr.build); --no-builds skips that step. --yes accepts all detected defaults non-interactively — useful for scripted or agent first-offers.
Walk the full ancestor chain and upload every save not already on the remote (oldest first) — for initial sync or after a gap. A single bulk presence check asks the remote which saves it already has, so only the genuinely missing ones are uploaded — a re-sync of a mostly-present history is fast instead of one round trip per save. Targets the named remote, or the default with no name, so koh offer --all origin sends the whole history to one remote. --history is a synonym; public remotes confirm once before publishing. A live progress line tracks the in-flight save; the closing summary names distinct failure reasons rather than a generic error.
Drain the offer queue — retry offers that failed because the remote was unreachable. Queued offers are stored in .koh/offer_queue.
Write a self-contained .face file to disk. A portable bundle of the current save's manifest and all its objects, applicable on any machine with koh apply.
Stealing
Initialize a local repository from a remote Kepr snapshot. Writes the source remote to .koh/remotes as origin. Prompts to add a private remote for backup. If the target save is flagged, shows the reason before any local files are written. --into <dir> clones into the given directory (created if absent) instead of the current directory — guards against cloning into an existing repo before any network call. On an unknown repo name, suggests close matches from the remote's public index. --key <path> specifies an SSH key for authenticated remotes.
Offer Status
Show offer status across all configured remotes, grouped per remote. Merges local .koh/offer_log receipts with live status from each Kepr instance. Status indicators: ✦ accepted, ◉ pending, ◯ queued (remote unreachable), ✗ rejected with reason.
Remove terminal offer log entries older than 90 days (default), or all terminal entries with --all. Pending and queued entries are always kept.
Other Kepr Commands
Apply a save from a Kepr instance by ID, or restore from a local .face file.
Delete a face or entire repository from the connected Kepr instance. See §10 Meta for full description.
14.5 Issues
List open issues on the connected Kepr repo. --closed shows only closed issues. --all shows open and closed together. --repo targets a specific Kepr repo URL without requiring a local project.
Show full detail on issue #n in the interactive pager. Includes the description, any maintainer notes, and the resolution face if the issue was closed with one.
File an issue against the connected Kepr repo. With no arguments, runs interactively — prompts for title, description, and optional contact email. With <title>, prompts for description only. With both, sends immediately. --repo targets a specific repo URL from any directory.
15 Bundles & Faces
Two portable file formats for working without a Kepr instance.
.face — a single save. Produced by koh offer --local. Contains one manifest and its objects. Use it to hand off a specific snapshot; the recipient applies it with koh apply.
.bundle — complete repository history. Produced by koh bundle. Archives the entire .koh/ directory as a gzip-compressed tar with a Blake3 content checksum. Use it to move or back up a whole repository; the recipient merges it with koh unbundle.
Both formats are ignored by Koh automatically — *.face and *.bundle files in your project directory will never appear in a snapshot.
Archive .koh/ as a deterministic tar+gzip .bundle with a Blake3 content checksum, written to the current directory.
Merge objects and manifests from a .bundle into the current repository. Existing objects are not overwritten. The database is updated to reflect the merged history.
16 Configuration
Each repository may have a .kohconfig at its root. Use koh config to open it. The parser is forgiving — unknown keys are silently ignored, malformed values fall back to defaults, missing file means all defaults apply. Never included in snapshots.
| Key | Default | Description |
|---|---|---|
| stable_days | 14 | Saves older than this (days) show a ◐ marker in koh log |
| promote_suggest | true | Show promote hint in log output when dev HEAD is promote-worthy |
| color | true | Disable with false, NO_COLOR, or KOH_NO_COLOR |
| page_size | 0 | Lines per pager screen. 0 = auto-detect from terminal row count |
| health_frequent_file_pct | 80 | % of saves a file must appear in to trigger the frequent-files signal |
| health_large_save_files | 20 | Files-changed count to trigger the large-save signal |
| health_undo_threshold | 3 | Revert count to trigger the undo-pattern signal |
| health_dev_age_days | 30 | Days without a promote to trigger the dev-lane-age signal |
| health_min_saves_per_week | 3 | Minimum recent save velocity before the velocity signal fires |
| health_storage_growth_pct | 200 | Month-over-month storage growth % to trigger the storage-growth signal |
| offer_on_save | unset | Remote name to offer to automatically after every save. Must be a private remote. |
| offer_on_save_async | false | When true, the background offer runs without blocking the save command. |
17 Storage Architecture
Disk Layout
SQLite Schema (v7)
Schema auto-migrates on first open. A SchemaTooNew error is raised if the database version exceeds what the binary understands, preventing silent data corruption.
| Migration | Change |
|---|---|
| v1 → v2 | Create tags table |
| v2 → v3 | Add size_bytes column to saves |
| v3 → v4 | Create marks table |
| v4 → v5 | Remove focus_dev from state; add agent, session, is_checkpoint to saves |
| v5 → v6 | Metadata migration for 12-character save IDs; existing 6-character IDs remain valid |
| v6 → v7 | Add flagged and flag_reason to saves; drop legacy tags and marks tables |
agent, session, and is_checkpoint fields outside the content checksum body. These fields do not affect verification — old saves continue to verify correctly. koh recover reads these fields to rebuild the saves table completely, including attribution and session grouping. Save flags are stored on the save record itself, so recovery rebuilds them with the rest of the manifest-backed metadata. The root LOG.md summary is rebuildable from Koh history, while the nested audit trail under .koh/log/ preserves the full history.
Safety Design
- Confirmation defaults to No — only lowercase
yconfirms, Enter alone cancels - Unsaved-changes guards on all history-altering commands (
koh checkpointis the deliberate exception) - Same-lane validation before
koh load - Append-only history — undo moves HEAD, never deletes
- Transactional saves with atomic manifest rename
- Manifest checksums verified by
koh verify - Object and manifest temp files are fsynced before their renames; the parent directory is fsynced after — power-loss durability, not just crash safety
- Database as a fully rebuildable cache — agent, session, is_checkpoint, and save flags all survive
koh recover - Orphan-safe cleanup via recursive SQL CTE
.kohignorechange warnings on restore- Flagged-save warnings before
koh steal koh revertlists untracked files before confirmation so no file is silently deleted- Failed
koh stealcleans up any partial.koh/automatically — retry is always safe koh recoverfully restores agent attribution, session grouping, checkpoint markers, and flagged-save metadata from manifests and the database- Advisory lock reclaim uses atomic rename — concurrent processes cannot both acquire a stale lock
- Schema migrations are transactional with idempotent column guards
Technology
Zig 0.16.0 in development, with a minimum supported build version of Zig 0.16.0. SQLite 3.49.1 is vendored as a C amalgamation. Compression uses system zlib (compress2/uncompress) in RFC 1950 format. Blake3 hashing comes from Zig's standard library, and HTTP for Kepr uses curl (subprocess) with a 60-second hard timeout.