Command reference
Every hav verb, grouped by area. All id arguments accept unique prefixes
(ambiguity errors list the candidates), and <knotish> means a knot id prefix
or a buoy name. Ids follow the display grammar.
Every command's output is single-line-per-fact, and errors are one line naming
the remedy, with a non-zero exit.
Except where noted, every mutating command is one undoable op, and every command begins by capturing the workspace.
Repo and status
hav init [--name <s>]
Create .hav/repo.db in the current directory, buoy main, and register the
directory as workspace ws-1 on a fresh untied knot. Writes no other files,
since .havignore is yours to create.
hav init --bare
Create an empty push/pull hub: no working tree, no workspace, no knots. Push creates buoys on first contact. Workspace commands in a bare repo error with a one-line explanation.
hav st
Capture, then show: the workspace knot and its description, its base
(on main@sn-…, plus · using <slug> when a crew is active), the tracked
diff (one line per changed variant: M/A/D path +a -d with path@N hunk tags
for text files, a readable sealed variant tagged [slug] with path[slug]@N
tags, an opaque one an M path [slug · sealed] stub with no counts),
outstanding conflicts (conflicted variants tagged [slug]), the
behind-notification (behind main by N knots — hav sync) when the tracked
buoy has advanced, and the undo cursor.
hav preview
What the next capture would ingest: total files and bytes plus a
per-top-level-directory breakdown, sorted by size, and the skipped-symlink
count. Same walker and ignore rules as capture. Reads nothing, stores nothing,
records no op. The tool for writing .havignore before a first capture on a
real project.
Naming and starting work
hav describe -m <msg>
Set the workspace knot's description: one line saying what the work is.
hav new [-m <msg>]
Start a fresh untied knot on top of the current one. The current knot keeps its state, and the workspace moves to a new knot based on it, with an empty diff.
hav discard
Abandon the workspace's untied draft: revert the tree to the draft's base,
start a fresh empty draft, retire the abandoned knot. Refuses tied history
(use hav new or edit instead, since dropping landed history is move,
pointer surgery) and drafts with knots stacked on them. No-op when the draft
is empty. Undoable: hav undo restores the work on disk.
Inspecting history
hav log [<knotish>] [-n N] [--buoys]
Walk knot ancestry newest-first from a knot or buoy (default: the workspace
knot), showing id, timestamp, description, snap, any buoys pointing there, and
any workspace holding it. The workspace's knot is marked @. Targeting a buoy
reviews history you don't have checked out. --buoys lists buoys and their
heads instead.
hav show [<knotish>]
One knot's identity: id, description, snap, created timestamp, the
buoys/crew/workspace on it, its parents (or base (untied)), and its diff vs
its first parent. The read a reviewer runs before tying an incoming knot.
Read-only.
hav diff [<knotish>] [--from <knotish> --to <knotish>]
Unified diff, one section per changed variant (a sealed variant suffixes
[slug] in its ---/+++ headers), hunks tagged path@N /
path[slug]@N (the tags split --take accepts). Members see real unified
diffs, and an opaque variant renders a stable one-line token per version
([variant · slug · <commitment-prefix>]). Default: the workspace knot vs its
base/first parent.
hav conflicts
List conflicted variants in the workspace knot: id, path, [slug] for a
crew's variant, shape (ours+theirs, base).
Buoys
hav buoy
List buoys with their head knot ids.
hav buoy <name> [--at <knotish>]
Create a buoy (default: at the workspace knot). Buoys reference tied knots
only, so pointing one at a draft is refused (tie first). Slashes in names are
fine (release/1.4).
hav buoy -d <name>
Delete a buoy. Workspaces tracking it become pinned. Undo restores both the buoy and the tracking.
hav move <buoy> --to <knotish>
Pure pointer move, with no merge and no solidify. --to is required because
this can abandon history. Tied targets only: tie is move-plus-solidify, and
move is pointer surgery over tied history.
Landing and syncing
hav tie [<src>] [<dst>] [--reveal]
The one merge verb. Defaults: the workspace knot into its tracked buoy. Semantics by destination, with the full story in Tie and sync:
- untied knot → buoy: the everyday land. Requires the knot's base chain
to ground on the buoy head (else
not in sync with <buoy> — hav sync <buoy>, naming the destination rather than the tracked buoy). Bases become parents, the buoy moves, and history stays linear. Split stacks land whole, bottom-up. The workspace then auto-starts a fresh draft on the new head. - tied knot → buoy: fast-forward when possible,
already containswhen redundant, otherwise a merge knot (genuinely diverged histories only) with conflicts stored inside its snap. - knot ↔ knot: a detached merge knot; self and ancestor pairs rejected.
- Sealed knots landing on a public buoy are refused without
--reveal, since that flag is the disclosure.
Reports K conflicts stored — hav conflicts when the result carries any.
hav sync [<buoy>]
Pull a buoy's advances into the workspace knot, explicit and never automatic.
3-way merge, conflicts materialize in this working tree (the buoy never
carries them), and the knot stays untied. A stale base (the knot beneath you
was rewritten by a cascade) refreshes in place. Default: the tracked buoy, and
pinned workspaces must name one (naming doesn't start tracking). Errors on a
tied knot (hav new first).
hav resolve <path> --take ours|theirs
Write the chosen side of a conflicted path, acting on the variant the workspace materializes (the active crew's conflicted variant if it has one, else the public conflict). Convenience only, since editing the file marker-free and running any command resolves it too.
Rewriting history
See Rewriting history for semantics; all four cascade to descendants where applicable.
hav edit <knotish>
Move this workspace onto an existing knot to change it in place. Each snap-changing capture appends to that knot and auto-rebases its tied descendants. One workspace per knot, ever (refused if another holds it). Pins the workspace and re-projects. Refuses retired knots.
hav squash [<knot>]
Fold the workspace knot (or the named one) into its parent/base: the parent takes the child's snap, descriptions concatenate, the child retires. The workspace moves to the parent, and sibling descendants cascade.
hav split --take <path>[@N]... --into-new -m <msg>
Additive split: move the selected whole files, or selected hunks (path@N /
path[slug]@N tags from st and diff; sealed-variant hunks need
membership), into a new knot beneath the current one, and the remainder
stays. The result is an untied stack, and a later bare tie lands it whole.
hav absorb
Route the workspace knot's edits into the untied ancestors that own the touched lines (per-line blame). Never guesses: ambiguous ownership, conflicted files, and anything owned by tied history stay put, and whole-file adds/deletes always stay. Working-tree bytes never change, and only attribution moves. Records nothing when nothing moves.
Workspaces
hav ws new <path> [--on <buoy> | --at <knotish>]
Create a new workspace: directory, .hav-ws pointer, projected files, fresh
untied knot based on the buoy head (tracking) or pinned knot. <path> must be
its own directory, so the repo root or an existing workspace is refused.
hav ws list
List workspaces: id, path, knot, base. Bare hav ws does the same.
hav ws -d <id>
Forget a workspace: drop its row and .hav-ws pointer, leaving the projected
files and the directory. The held knot becomes an unheld draft. Refuses the
current workspace, and asks for confirmation when the knot holds untied work
no buoy reaches. Undoable (undo restores the row; rerun ws new if you need
the directory reprojected).
Undo and the journal
hav undo / hav redo
Move the op cursor: restore the recorded view and re-project the files on disk. Output always names what changed and whether redo is available. See Undo and the op journal.
hav op log [-n N]
The numbered op list (op-412 capture sn-19ac (+2 ~1)), cursor marked @.
hav op limit [<n>]
Show or set journal retention (default 50; 0 = unlimited). Undo reaches back
exactly this far. Lowering below what's retained discards history and asks for
an explicit y. The configured limit is standing consent for routine pruning.
Records no op.
hav sweep
Drop drafts and objects nothing retained can reach, dissolve packs that held
dropped members, and compact the database. This is the physical complement of
op limit. Explicit, workspace repos only, and refuses while any sealed crew
is locked (run it where your keys are). Records no op.
Remotes
See Remotes and hubs.
hav remote [<name> <path-or-url> [--token <t>] | -d <name>]
List, add/update, or delete remotes. A remote is a path to another repo or a harbor URL. The token gates pushes only, since harbor reads are anonymous. Add and delete are undoable, and tokens live outside op views.
hav clone <src> <dir> [--token <t>]
Copy every buoy's tied history from a path or harbor URL into a fresh repo at
<dir> (refused if it exists), set up ws-1 tracking main, project files,
record origin. No token is needed to clone, and --token stores the push
credential for later.
hav push [<buoy>] [--remote <name>]
Send tied history to the remote and fast-forward its buoy (created if absent).
Fast-forward only, refused when the remote is ahead or unknown
(hav pull first), with the check running inside the remote's write
transaction. Untied WIP, workspaces, and the op journal never travel. Rewrites
reaching published history go through with a loud warning. Defaults: tracked
buoy, remote origin. Records no local op.
hav pull [<buoy>] [--remote <name>]
Fetch the remote head's tied history and fast-forward the local buoy (created
if absent). Never merges, never touches the working tree. Tracking
workspaces get the ordinary behind — hav sync notification. On divergence
the fetched data stays and pull prints the recipe (hav tie <remote-head> <buoy>). One op when anything changed.
Keys and crews
See Sealed crews.
hav key [gen]
Show this machine's public key, or create the identity keyfile
(~/.config/hav/identity; $HAV_IDENTITY overrides). Once per person, and
private keys never move. Back the keyfile up, because losing it
permanently loses all sealed-crew content.
If the keyfile resolves to a path inside a worktree, capture refuses rather
than publishing your private key. Move it out of the repo or add it to
.havignore.
hav key rotate [--finish]
Swap this machine's key with a two-key overlap: rotate mints a fresh
primary while the old key stays on a second line of the keyfile, still
unwrapping everything it could, so there is no lockout window. Get the new pk
added to every crew the old key is in (hav crew <slug> --add <new-pk>, which
any member can do, this machine included, in every repo that has the crew),
remove the old pk, then --finish drops the old line (it asks y/N first, and
declining drops nothing). hav key lists the old key while the rotation is in
flight, and a second rotate is refused until you finish.
Identity is per-person rather than per-repo: rotation is not an op, and hav cannot enumerate which crews across which repos still need the new key. That inventory is yours. A crew still wrapped only to the old key when you finish becomes unreadable from this machine, forever.
hav crew
List crews. Members see the sealed display name; others see slug and counts.
hav crew <slug> --name <s>
Create a crew, or rename its display name. The slug is immutable and public,
and the display name is sealed. Buoys live under private-<slug>/….
hav crew <slug> --add <pk> / --remove <pk>
Add a member (wrap the crew key to their public key) or remove one (rotate to
a fresh generation for everyone who stays; any member can, the creator
included). Removing the last member warns: the sealed history becomes
permanently unreadable (hav undo restores it while the op is retained). All
undoable ops.
hav crew use <slug> / hav crew use --none
Set or clear this workspace's active crew (membership required; local
state, one undoable op). The active crew's
variants
materialize on disk and receive your edits at variant paths, and with none
active you see and edit the public variants. use and none are reserved
words, never valid slugs. Fresh workspaces and clones start with none.
Re-running the current setting rematerializes in place (the repair for an
interrupted swap). On a sealed knot the setting applies only when it names
the knot's own crew;
another crew's setting records but stays inert (the output and st say so),
its variants carried untouched.
hav crew -d <slug>
Delete a crew from this copy: its config (row, wraps, key chain) and its
sealed buoys, one undoable op. No membership needed, since whoever holds a
copy is sovereign over it. Asks y/N, naming what dies. The crew's sealed
history becomes unreadable here (hav undo restores it while the op is
retained, and hav sweep frees the ciphertext once nothing retained reaches
it). Refused while any workspace has the crew active or sits on one of its
knots. Local only: remotes are untouched, and a later pull that carries the
crew's content, or wraps your key, brings the config back. The slug is
immediately reusable here, but the recreation is a brand-new crew
(identity, not name), so
copies that knew the old one will never accept the new one under that slug.
hav seal <path> [--keep-public]
From this capture on, the file at <path> stores as the active crew's
sealed variant
(variant sets).
Drops the public variant going forward unless --keep-public, and
already-published versions stay public history forever. There is no rule and
no pattern: the variant in the snap is the routing. On a sealed knot only
the knot's own crew
can seal (the variant outlives the knot's reveal); another crew's active
setting is refused there.
hav unseal <path>
Publish the active crew's variant at <path> as the public variant and drop
the sealed one going forward. A disclosure: names what it publishes and asks
y/N. Sealed history stays sealed. Inside the crew's own sealed buoy the
"public" slot is still the knot's ciphertext, so the content goes plaintext
only when the knot is revealed.
The harbor
See The harbor.
hav harbor serve <root> [--addr host:port] [--token <t>]
Serve every bare repo under <root> at http://<addr>/<name>. Same
push/pull/clone verbs, URLs in place of paths. Reads (pull and clone) are
anonymous, and the token (--token or HAV_HARBOR_TOKEN, required) gates
every push.
Storage
hav pack
Compact loose objects into two-regime storage: large blobs content-defined chunked and deduped, everything else into solid zstd packs. Ids and reads are unchanged, and packs expand back to whole objects on the wire. Runs on a workspace repo or a bare hub. Records no op.