Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70
kit — CLI reference
Short user-facing reference for the kit command. For wire-format
details see the spec docs in this directory (SPEC-OBJECTS, SPEC-INDEX,
SPEC-REFS, SPEC-PACKFILE, SPEC-TRANSPORT, SPEC-SIGNING, SPEC-FASTCDC,
SSH-SECURITY).
Quick start
kit init # create .kit/ in the current directory
kit keygen # generate an Ed25519 signing key
echo hello > README.md
kit add README.md
kit commit -m "first commit"
kit log
Commits are signed with your Ed25519 key and stored locally. Your author Identity is automatically derived from your signing key's public key — no config needed.
Global flags
These are parsed before the subcommand (so they apply to repo discovery too), like git:
kit -C <path> <command>— run as ifkitwere started in<path>. Repeatable; relative paths resolve against the prior-C(git semantics).kit -c <key>=<value> <command>— set a config value for this one invocation. It is applied to the effective config only (never written to disk) and flows through the same enforcement as a per-repo config file: security-sensitive keys (user.identity, signing/transport trust — theREPO_FORBIDDEN_KEYSset) are refused with a warning. So-ccannot spoof the signed author or redirect trust; use it for repo-safe keys (for example,-c default_branch=trunk).
--git-dir, --work-tree, and --exec-path are non-goals — kit uses
the .kit/ marker and has no split work-tree / exec-path model.
Subcommand reference
Working-tree commands:
kit init— create a new repository in.kit/.kit add [-A|-u] [-f] [-p] <path>.../kit add .— stage files for the next commit. Multiple pathspecs may be given..stages every non-ignored file under the current directory.-A/--allstages every change in the worktree including deletions of tracked files (takes no path arguments).-u/--updaterestages only already-tracked files — updating modified ones and recording deletions — without adding untracked paths (takes no path arguments).-Aand-uare mutually exclusive. An explicitly-named path that is ignored (by.gitignore/.kitignore) is refused unless-f/--forceis given (git parity); already-tracked paths are never subject to ignore.-p/--patchinteractively presents each hunk between the staged (or HEAD) blob and the worktree file and stages only the accepted ones — per hunk:ystage,nskip,astage this and all later hunks,dskip this and all later hunks,qquit (keeping hunks already chosen). It requires explicit path arguments, is incompatible with-A/-u, and operates on regular text files only: a binary file is skipped with a message (the command still succeeds), and symlinks/directories are refused. An explicitly-named ignored path is refused unless-f/--force(like plainadd); a path that escapes the repo through a symlinked parent is refused. The hunk view and prompts go to stderr;s(split) ande(manual edit) are follow-ups.kit rm [--cached] [-r|--recursive] [-f|--force] <path>...— remove paths and stage the deletion for the next commit. By default this deletes the worktree file(s) and stages the removal; now-empty parent directories are pruned. Multiple pathspecs are accepted.--cached— stage the removal only, leaving the worktree file(s) on disk (the historical kit behavior).-r/--recursive— required to remove a directory and everything under it; without it, a directory pathspec is refused.-f/--force— remove worktree files even when their content has diverged from the staged blob. Without--force,rmrefuses to destroy a locally-modified tracked file (a dirty-worktree guard in the spirit of the restore guards); use--cachedto keep the file or--forceto discard the local changes.
kit mv [-f|--force] <source>... <dest>— move or rename a tracked path and stage the change.mv <src> <dst>renamessrctodst, or moves it intodstwhendstis an existing directory; with more than one source,<dest>must be an existing directory. The worktree file is moved and the index updated (the source staged as removed, the destination staged with the source's blob and mode — content is unchanged, so the existing object is reused). Because the blob keeps the same content id at the new path,kit status/kit diffdetect the move as an exact rename and report git'sRby default;--no-renamesshows the underlying deletion plus addition instead.-f/--force— overwrite an existing destination. Without it,mvrefuses to clobber an existing path (matching git'smvguard) — a kit data-loss guard; a dangling symlink at the destination counts as existing (git refuses that too). A missing or untracked source is refused. All sources are validated before any move, so a bad source in a batch never leaves the worktree half-moved. As a kit safety divergence, a destination that escapes the repository through a symlinked parent directory is refused (git would silently follow it).- Directory sources are supported:
mv dir newdirrenames the directory, andmv dir existing-dir/moves it into the target (becomingexisting-dir/dir). The directory is renamed in a single filesystem operation, so untracked files inside it travel with it (exactly likegit mv), and every tracked file beneath it is restaged at its new path (each a delete plus add, per the no-rename-detection note above). The repo-escape and validate-before-move guards all apply. A directory move refuses a pre-existing destination even with-f(recursively removing it would strand its tracked files in the index and delete untracked ones — git won't clobber a directory withmveither), rejects overlapping sources up front (mv dir dir/file <dest>), and refuses moving a directory into itself.
kit status [--porcelain[=v1|v2]] [-s|--short] [-z]— show staged and unstaged changes. Default-mode prose (banner plus section headers plus per-file lines) goes to stderr; stdout is reserved for machine output.-s/--shortis an alias for--porcelain=v1; both select the same renderer. Porcelain emits one entry per line ingit status --porcelain=v1format (XY <path>, with kit'sTforModeChangedas the only non-git extension).--porcelain=v2selects git's richer per-path format —1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>for tracked changes (octal modes; full 64-hex BLAKE3 object ids;<sub>alwaysN...) and? <path>for untracked — with no rename (2) records and no--branchheader lines. Paths with special bytes are C-style quoted (matching git's defaultcore.quotePath).-z(which implies porcelain) NUL-terminates records and emits raw, unquoted paths — the round-trip-safe form for paths containing newlines or other special bytes. Empty stdout means clean.kit diff [--staged|--cached] [--name-only|--name-status|--stat] [--merge-base] [-w|-b] [-U<n>] [-z] [<rev> [<rev>] | <a>..<b> | <a>...<b>] [<path>...]— show changes as a unified patch. With no arguments, compares the HEAD tree to a fresh worktree snapshot.--staged(alias--cached) compares the HEAD tree to the staged index tree — the changekit commitwould record. A single revision compares that revision against the worktree; two revisions (or an<a>..<b>range) diff the two resolved trees directly; a symmetric<a>...<b>range diffs the merge base ofaandbagainstb(git semantics).--merge-baseis the flag spelling of that symmetric range: with two revisions it diffsmerge-base(a, b)againstb, and with one revision it diffsmerge-base(<rev>, HEAD)against the worktree (likegit diff --merge-base). Revisions may be refs, commit hashes, short hashes, orHEAD~n(a raw 64-hex tree hash also works, since it resolves to itself). Any remaining positional arguments are pathspecs that limit the output to entries at or below them. The default output is a git-shaped unified diff: adiff --git a/<path> b/<path>header per changed path (withnew file mode/deleted file mode/old mode/new modelines, anindex <old>..<new> <mode>line, and--- a/<p>/+++ b/<p>using/dev/nullfor adds/deletes) followed by@@-delimited hunks (orBinary files … differfor non-text blobs). The hunk algorithm is the Myers diff with git's change-compaction, so the output byte-matchesgit diffapart from the abbreviatedindexids, which are BLAKE3 prefixes rather than SHA-1 (git's optional indent heuristic is not applied).--name-onlyinstead lists one changed path per line;--name-statusprefixes each with a status letter (A/D/M;Tmarks a kit mode change).--statshows a diffstat: one<name> | <count> <graph>row per file (name column padded,+/-graph scaled to the terminal width likegit diff --stat, honoringCOLUMNS; default 80) followed by aN files changed, … insertions(+), … deletions(-)summary; binary files renderBin <old> -> <new> bytes. In--name-only/--name-status/--statmodes special-byte paths are C-style quoted (gitcore.quotePath);-zinstead NUL-terminates records and emits raw paths (in--name-status -zthe status letter and path are each their own NUL-terminated field).-zis only valid with--name-only/--name-status.--exit-codeexits 1 when there are differences (0 when none), still printing the patch;--quietis the same but prints nothing — the CI idiom for "fail if the tree changed".--color[=always|auto|never]colorizes the patch (git's palette: bold metadata, cyan hunk headers, green additions, red deletions); the defaultautocolorizes only on a tty (honoringNO_COLOR/CLICOLOR_FORCE), and--no-colorforces it off. (status,logandbranchdo not use color.)-w/--ignore-all-spaceignores whitespace when comparing lines (a line that differs from its counterpart only in whitespace shows as unchanged context — like git);-b/--ignore-space-changeis the weaker form, where differing amounts of whitespace compare equal but a line with whitespace where the other side has none still differs.-wwins if both are given. Either way the printed hunk lines show their own real (unmodified) bytes — only line comparison changes.-U<n>/--unified=<n>sets the number of unchanged context lines around each hunk (default 3, matching git's-U3).kit worktree add <path> [<commit-ish>]/list [--porcelain]/remove [--force] <path>/prune [--dry-run]— manage linked working trees (git-worktree parity). Linked trees share the one object store and the shared refs; each tree keeps its ownHEAD, index, in-progress-operation state, and stash (note: git shares the stash across worktrees; kit's stash is deliberately per-tree).addwith no<commit-ish>creates a new branch named after the path's basename (refusing if it already exists); with a branch name it checks that branch out, and with any other revision the new tree starts on a detached HEAD. A branch may be checked out in at most one tree at a time —worktree add,checkout/switch,branch -d, andbranch -mall refuse withalready checked out at '<path>', naming the holding tree (branch moves are single-writer by design; see SPEC-HISTORY-PROOF).removerefuses when the tree has local changes, untracked files, or an operation in progress unless--forceis given, and never removes the main tree or the tree you are standing in.prunedeletes registry entries whose tree has vanished (--dry-runpreviews). On-disk: a linked tree's.kitis a pointer file (kitdir: <path>) into the main repository's.kit/worktrees/<id>/. Interim limit:kit gcrefuses to run while linked worktrees exist (cross-tree root collection is a tracked follow-up) — remove or prune them first.kit stash [save|list|pop|apply|drop|clear|show] [--format=json]— save/restore WIP changes.applyrestores an entry without removing it;cleardrops every entry.pop/apply/drop/showselect an entry by index (default0), accepting either a bareNor thestash@{N}form printed bystash list.pop/applyaccept--index, which also restores the staged state recorded when the stash was created (likegit stash pop --index), not just the worktree changes.saverecords that staged snapshot as the stash commit's second parent (git-style[HEAD, index]) — no on-disk manifest change, and it stays gc-reachable through the stash commit. The snapshot is the serialized index (a blob in the index commit's wrapper tree, paired with a content subtree that keeps staged blobs gc-reachable), so--indexrestores the index exactly, including staged deletions (kit rm-staged) — which a tree could not encode. Stashes created before this feature carry no index snapshot, so--indexis a no-op for them (with a note).--format=json(a global flag, valid after any subcommand) emits a machine-readable result to stdout: onlist, JSONL with keysindex,hash,message(mirroringbranch --format=json); onsave/pop/apply/drop/clear, a single{"ok":true,"kind":"…", ...}object; on failure,{"ok":false,"error":"…"}.kit sparse-checkout— manage sparse checkout patterns.
History / commits:
kit commit [-a|--all] [--amend] [--author <spec>] [-m <msg> | -F <file>] [--format=json]— create a signed commit from the staging index. The index is built bykit add/kit rm;commitwith an empty index is an error. Usekit add .to snapshot the whole worktree before committing.-msupplies the message inline;-F/--filereads it from<file>(likegit commit -F), with-meaning standard input —-mand-Fare mutually exclusive, and omitting both launches$EDITOR.-a/--allfollows Git's tracked-only shortcut: it stages modified tracked files and tracked deletions before committing, but does not add untracked files.kit commit -am <msg>is accepted as shorthand for-a -m <msg>.--author <spec>overrides the recorded author Identity for this commit (highest precedence — aboveuser.identityconfig and the signing-key-derived default). The<spec>grammar ised25519:<hex>(32-byte public key),did:key:<multibase>(the multibase payload afterdid:key:, for exampledid:key:z6Mk…, stored verbatim — must be a non-empty printable-ASCII multibase string), oropaque:<bytes>(raw UTF-8); note this differs from theuser.identityconfig grammar (which also acceptsmid:<N>and raw encoded hex). The signature is always produced by your signing key regardless of the recorded author.--amendreplaces HEAD instead of adding a new commit: the new commit re-uses HEAD's parent(s) as its own parent(s), takes its tree from the index, and is re-signed; the branch is moved to it and the canonical ancestry snapshot is updated when history proofs are enabled. Without-mthe previous commit's message is reused (no editor is launched). The superseded commit is recorded in the recovery log so it stays recoverable, and is reclaimed bykit gconce it falls out of the retention window. Afterkit merge --no-commit(or a merge you have just resolved),commitnotices the recordedMERGE_HEADand records a two-parent merge commit rather than an ordinary single-parent one. On successcommitprints git's summary on stderr:[<branch> <hash>] <subject>(with(root-commit)for the first commit,detached HEADwhen detached) followed by a diffstat andcreate/delete modelines;-q/--quietsuppresses it. kit always signs commits and has no hooks.--format=jsonadditionally emits one JSON object to stdout on success:{"ok":true,"hash":"<64-hex>","branch":"<name>|null", "parents":["<64-hex>",...],"tree":"<64-hex>","subject":"...", "is_merge":<bool>,"is_root":<bool>}.kit log [--oneline] [--abbrev-commit] [--abbrev[=N]] [--format=json] [--author <pattern>] [--grep <pattern>] [--since <date>] [--until <date>] [--no-merges] [--first-parent] [-n N] [<rev> | <A>..<B> | <A>...<B>]— show commit history. With no argument the walk starts atHEAD; an optional<rev>starts it there instead, a range<A>..<B>shows commits reachable fromBbut not fromA(an empty side meansHEAD, soA..isA..HEADand..BisHEAD..B), and a symmetric range<A>...<B>shows commits reachable fromAorBbut not their common ancestors (the merge base). Commits are ordered reverse-chronologically with a topological tie-break (a parent never precedes a child) — git's--date-order, which matches git's default for linear and monotonic-timestamp history (it can differ only on merge DAGs with skewed/imported timestamps). The default format prints the full commit message body, indented by four spaces, and renders the timestamp as a stable UTC date in the formYYYY-MM-DD HH:MM:SS +0000.--onelinecondenses each commit to<abbrev-hex> <title>.--abbrev-commitabbreviates the commit id in the default format too, and--abbrev[=N]sets the abbreviation length (bare--abbrevand--onelinedefault to 7; the length is clamped to[4, 64]). Note the abbreviated id is a BLAKE3 prefix, so it is longer than a Git SHA-1 prefix of the sameN.--format=jsonemits JSONL (one JSON object per commit, newest first) with keyshash,parents,tree,author,timestamp,title,message; thetimestampstays a raw Unix-seconds integer for machine consumption.--author <pattern>and--grep <pattern>filter the walk by a plain substring match —--authoragainst both the short display form (theAuthor:line) and the fullkind:hex/mid:Nform (--format=json'sauthorfield);--grepagainst the full commit message (title plus body). This is intentionally not a regex: unlike git, kit identities are opaque (Ed25519 keys,mid:Nnumbers, DID keys), not free-textName <email>.--since <date>/--until <date>bound the commit timestamp; accepted forms are@<unix-seconds>,now/today/yesterday,<N> <unit> ago(second/minute/hour/day/week/month/year),YYYY-MM-DD(midnight UTC), andYYYY-MM-DD HH:MM:SS/YYYY-MM-DDTHH:MM:SS[Z]— a small, explicit grammar, not git's full natural-languageapproxidate.--no-mergeshides merge commits (more than one parent) from the output without changing the walk;--first-parentis stronger — it follows only each commit's first parent, so a merged side branch never enters the walk at all. All of these filters apply before-n <limit>, so-ncaps the filtered result, matching git.kit reflog [<ref>] [--format=json] [-n N]— read-only view of a branch's first-parent ancestry. Defaults to the branchHEADpoints at; detachedHEADneeds an explicit<ref>. Walks newest-first, addressing each entry<ref>@{N}(@{0}is the current tip). With--features history-mmr, inclusion proofs are checked against the current canonical snapshot and its descriptor, which binds repository, branch, generation, tip, count and root.--format=jsonemits JSONL with keysref,selector,index,hash,title,ancestry_verified(true/false/null). Superseded commits after amend/reset are outside this reachable chain; recovery records are separate.kit blame [--format=json | --porcelain | --line-porcelain] [-w] [-M] [-C] [--ignore-rev <rev>] [--ignore-revs-file <file>] [--ignore-rev-precise] [--first-parent] [--reverse] [-L <start>,<end>] [<rev>] <file>— show line-level commit attribution.--porcelainemits git's grouped machine format — a per-line header (<id> <orig> <final> [<group-len>]) followed by a metadata block (author/committer,author-time/-tz,summary, aboundarymarker on a file-history root, andfilename) once per commit, with each content line tab-prefixed;--line-porcelainrepeats the full block for every line. kit's documented divergences (consistent with--format=jsonand thelogprecedent): object ids are 64-hex;author/committercarry kit's Identity string, notName <email>(author-mail/committer-mailare empty and both*-tzare+0000, since kit commits store a single UTC author plus timestamp);filenameis the-Ccopy source on a cross-file copy; git's out-of-scopepreviousline is not emitted.-wignores whitespace when matching lines across revisions (likegit blame -w, ignoring all whitespace), so a whitespace-only edit — reindent, tab↔space, spacing tweak — doesn't reattribute the line; output still shows the file's current bytes.-Mdetects lines moved within the file and-Clines copied from other files (likegit blame -M/-C;-Cimplies-M, and repeating it widens the search:-Ccovers files changed in the commit,-C -Cevery file in the commit that creates the blamed file, and-C -C -Cevery file in any commit — a whole-tree search at each walk step, so a block copied from an unmodified source is still found). Detection is block-based: the longest contiguous moved/copied block above git's default thresholds (20 alphanumeric characters for-M, 40 for-C) is credited to its origin, so a moved block next to genuinely-new lines is still split out, and combined with-wa block copied with a whitespace change is still detected. The inline numeric forms override the default threshold, like git:-M<num>/-C<num>(glued, no space) set the minimum alphanumeric character count, and a numeric-C<num>still counts toward the-Clevel (so-C40 -Cis level 2 with threshold 40). The percent form-M<num>%/-C<num>%is accepted, but because kit's block detector has no similarity-ratio model the number is treated as the same char-count threshold — a deliberate,log-consistent divergence. Remaining divergence from git: a single unmatched run over 10,000 lines is matched only as a whole, not by sub-block. Move/copy detection is merge-aware and implements git's per-parent mechanism (pinned against git 2.50.1): at a merge, every real parent is offered the unexplained lines in commit order, first-found-wins. A parent that contains the blamed file (and whose copy of it isn't byte-identical to an earlier parent's) supplies the within-file-Msource, and its-Ccopy candidates are the files modified between that parent and the merge; a parent without that — because it deleted the file, holds a duplicate of an earlier parent's copy, or the file is newly added by the merge — has no-Msource, and with-C -Cits entire tree is searched instead. Everyday consequences (each pinned by a test with its git recipe): a block copied in from a side branch is credited to that side; an unchanged source file on the first parent is invisible (the block stays on the merge) while the same source modified in the merge does credit the first parent; a parent that deleted the blamed file can still supply the copy source; and for a file newly added by the merge every real parent is searched, first included.-Mand--ignore-revtreat every relevant parent uniformly (first-parent-wins); under--first-parentall of the above runs against the first parent only.--ignore-rev <rev>(repeatable) and--ignore-revs-file <file>skip "noise" commits — mass reformats, license-header sweeps, renames — during attribution, likegit blame --ignore-rev: a line that would be credited to an ignored commit falls through to the commit that previously changed it, while a line the ignored commit genuinely inserted stays put.--ignore-revaccepts any revision (short hash, ref,HEAD~2);--ignore-revs-fileentries must be full hex object names, one per line, with blank lines and#comments (including inline) skipped — both matching git. A file over 16 MiB is a data error. (kit does not auto-read a.git-blame-ignore-revsfile or ablame.ignoreRevsFileconfig key; pass the file explicitly.)--ignore-rev-precise(kit-only; requires--ignore-revor--ignore-revs-file— a usage error otherwise) refines that fall-through with content matching instead of git's positional per-hunk guess: git pairs the k-th added line of a changed hunk with the k-th removed line purely by position, so a reformat/reorder can land a line on the wrong parent counterpart (or leave it credited to the noise commit as a "genuine insertion" when the hunk has no local counterpart at all); kit hashes line content, so it searches the whole parent file — not just the enclosing hunk — for the line's true surviving origin, and only overrides the positional guess when content evidence disagrees with it. Composes with-w(matching is over-w-normalized keys, same as-M/-C) and with merges (the search runs per relevant parent, first-parent-wins, same as the positional default). This is a documented divergence — the default--ignore-revfall-through stays byte-identical to git; see "Divergences from Git" below. Blame is merge-aware by default (likegit blame): at a merge, a line is credited to whichever parent's side actually wrote it, so a line merged in from a side branch is attributed to its authoring commit rather than the merge.--first-parent(likegit blame --first-parent) follows only each commit's first parent, so such a line is instead credited to the merge commit — the older linear-history behavior; it composes with-w/-M/-C/--ignore-rev.-Lrestricts output to a line range —<start>,<end>,<start>,+<n>(n lines forward),<start>,-<n>(n lines back, ending at<start>),<start>,(to EOF),,<end>(from start of file), or a bare<start>(to EOF); bounds are 1-based and inclusive, an inverted range is swapped, and an over-long end is clamped to EOF, matchinggit blame -L. An optional<rev>(a ref, hash, orHEAD~2-style spec) blames the file as of that revision instead ofHEAD.--reverse <start>..<end>walks history forward instead of backward (likegit blame --reverse): it blames the<start>version of the file and attributes each line to the last commit in the range in which it still existed — useful for "which commit removed or last touched this line."<start>..defaults<end>toHEAD; an explicit<start>is required (kit reports a clear error for a missing range or open start, wheregitprints a cryptic "dig up from" message — a deliberate divergence, like the-Ldiagnostics). A line that never survives a step stays on<start>(git marks such lines with a leading^; kit's tab format carries no^, consistent with its existing boundary-marker omission).-wand-Lcompose with--reverse. Default emits<short12>\t<line_num>\t<text>per line;--format=jsonemits JSONL with keyshash,line_num,author,timestamp,text.kit verify <rev> [--trusted] [--trust-roots <path>]— verify the signature on a commit, remix, or signed tag.<rev>is an object hash, a branch/tag name, orHEAD; a tag name resolves to its annotated-tag object when one exists. By itself this only proves the object's own embeddedsignerproduced the signature — it does NOT checksigneragainst any allow-list, so a signature from an unrecognized key verifies identically to one from a key you actually trust. Pass--trusted(or--trust-roots <path>) to additionally cross-checksigneragainst the trust-roots registrykit trustmanages, failing closed (nonzero exit, distinct from a bad-signature failure) on an unlisted signer.--trust-rootsdefaults to$XDG_CONFIG_HOME/kit/trust-roots.toml; an in-repo path is refused unless passed explicitly (seekit trustbelow anddocs/THREAT-MODEL.md§5). A verified signature proves the embedded signer produced the commit; it does not bind that signer to a person or organization.kit prove <revision> [<path>] [--chunk N | --range OFFSET:LEN] [--with-offsets] [-o FILE] [--format=json]— build a disclosure bundle proving that<path>(repository-relative, split on/; omitted: the root tree), chunkNof a ChunkedBlob, or byte rangeOFFSET:LENbelongs to<revision>(a commit or remix).--chunkand--rangeare mutually exclusive;--with-offsetsis only valid with--range.LENmust be greater than 0. Writes the bundle to-o FILE, or to stdout when stdout is not a TTY (refuses to write binary to a TTY). Default status line:proof: <bytes> B for <path> @ <short id> (<kind>)(stderr when the bundle goes to stdout).--format=jsonemits{commit_id, path, selector, bundle_bytes, bundle_blake3, output}. Missing path/object exits66(NOINPUT); a range that crosses a chunk boundary or an invalid selector exits65(DATAERR).kit verify-proof <commit-id> <bundle-file|-> [--expect-path PATH] [--trusted] [--trust-roots PATH] [--format=json] [--payload-out FILE]— verify a disclosure bundle against a trusted 64-hex commit id.<commit-id>is not resolved as a revision.-reads the bundle from stdin.--expect-pathcompares the authenticated path toPATHand fails with65(DATAERR) on mismatch — the SPEC-DISCLOSURE rule that the caller compares the returned path.--trusted/--trust-rootsreusekit verify's trust-roots flow againstDisclosed.signer;signature_valid == falseis reported and, with--trusted, is a failure. A verified bundle proves the content belongs to the commit id; binding the signer to a person or org stays application policy (trust roots). Default output mirrorskit verify:ok: <kind> <path> @ <short id>, <bytes> B, signer <keyid> (valid|INVALID signature)[, signer trusted]orbad: <reason>.--format=jsonprints the disclosure report plussigner_trusted(true/false/null), includingstep_inner_roots(hex array) andchunk_inner_root(hex ornull).--payload-out FILEwrites the verified payload bytes.kit cat <hash>— display an object by its hash.kit hash <file>— hash a file and store it as a blob.kit tree— snapshot the working directory as a tree object.
Read-only plumbing (object/ref inspection, for scripts and agents):
kit cat-file (-t | -s | -p) <object>/kit cat-file --batch— inspect a stored object (likegit cat-file).-tprints the type (blob/tree/commit/tag; kit'sremixis the one non-git type);-sprints the size (the content byte length for blobs — matching git — but kit's serialized size for trees/commits, which differs);-ppretty-prints (a blob's raw bytes, a tree as<mode> <type> <hash>\t<name>lines, or a readable commit/tag summary).--batchreads object names from stdin (one per line) and emits, per object, a<hash> <type> <size>header line followed by the content and a trailing newline (<name> missingfor unknown objects);<size>is the byte length of the content that follows, so blobs are byte-exact with git while commit/tree content (and its size) is kit-shaped, as with-p.<object>resolves through the shared revspec grammar (hash, ref,HEAD,HEAD~n/^).kit show [--stat] [<object>...]— display objects (likegit show), defaulting toHEAD. A commit/remix prints a header (commit <hash>,Author:,Date:, message indented four spaces — matchingkit log) followed by the unified diff against its first parent; that diff body is produced by the same code askit diff, soshow <commit>is byte-identical todiff <parent> <commit>(modulo abbreviatedindexids).--statshows a diffstat instead of the full patch for commit/remix objects (likegit show --stat); non-commit objects are shown as usual. A tag prints its header then the peeled target object; a tree is listed as<mode> <type> <hash>\t<name>lines; a blob prints its raw contents. Multiple objects may be given. As withlog, the commit/tag header carries kit's signedIdentityand 64-hex BLAKE3 ids, so those header lines diverge from git'sAuthor: Name <email>/ 40-hex form (the same documented divergence aslog); the diff body, tree listing, and blob output match git.<object>resolves through the revspec grammar.kit ls-tree [-r] [-z] <tree-ish> [<path>...]— list a tree's entries as<mode> <type> <hash>\t<name>(git octal modes100644/100755/120000/040000; the hash is 64-hex BLAKE3).-rrecurses, showing leaf blobs with full paths and omitting tree lines (like git);-zNUL-terminates records and emits raw paths (otherwise special-byte paths are C-style quoted). Trailing pathspecs limit the listing.kit ls-files [-s] [-z] [--others] [--ignored] [--exclude-standard]— list files in the index or untracked worktree files (likegit ls-files). Default prints tracked paths, one per line, sorted.-s(--stage) prints stage info as<mode> <hash> <stage>\t<path>(git octal mode;<stage>is always0— kit has no merge stages).--otherslists untracked worktree files instead;--exclude-standarddrops ignored ones (per.gitignore/.kitignore) and--ignoredinverts to show only ignored files (--ignoredrequires--others, like git's-ioutside an-o/-cselection).-zNUL-terminates records and emits raw paths (otherwise special-byte paths — including under-s— are C-style quoted).kit rev-parse [--short[=N]] [--abbrev-ref] [--show-toplevel] [<rev>...]— resolve revisions to object ids. Bare<rev>...prints each resolved 64-hex id;--short[=N]abbreviates (default 7, a BLAKE3 prefix);--abbrev-ref HEADprints the current branch; kit always errors on an unresolvable revision;--show-toplevelprints the repository root (the directory holding.kit, found by walking up from cwd).kit rev-list [--count] <rev>— list the commit ids reachable from<rev>in reverse-chronological (topological) order, one 64-hex id per line;--countprints the number instead. Reuseslog's walk so the order matches. (Range/filter forms are a follow-up.)kit merge-base [--is-ancestor] <a> <b>— print the best common ancestor of two commits (64-hex; exit 1 with no output when there is none).--is-ancestortests whether<a>is an ancestor of<b>, exiting 0 (yes) or 1 (no) with no output. Reuses the merge engine.kit show-ref [--heads] [--tags]— list refs as<hash> <refname>, sorted by ref name. Default showsrefs/heads/*,refs/tags/*, and anyrefs/remotes/*tracking refs;--heads/--tagsfilter to one namespace.kit for-each-ref [--format=<fmt>] [<pattern>...]— iterate refs (heads, tags, and remote-tracking refs), sorted by ref name (likegit for-each-ref). The default line is<objectname> <objecttype>\t<refname>.--formatsubstitutes%(atom)tokens (refname,refname:short,objectname,objectname:short,objecttype;%%is a literal%); the object id is 64-hex BLAKE3. Optional<pattern>arguments filter to refs whose full name equals or is under each pattern.kit symbolic-ref [--short] <name> [<ref>]— read or write a symbolic ref (currently onlyHEAD), likegit symbolic-ref. Without<ref>, prints the full target (refs/heads/main), or just the branch name with--short; errors when HEAD is detached / not symbolic. With<ref>(which must be underrefs/heads/), repoints HEAD at that branch — the plumbing form, which does not touch the worktree; the branch need not exist yet (matching git).kit update-ref [-d] <ref> [<newvalue> [<oldvalue>]]— create, update, or delete a ref underrefs/heads/orrefs/tags/(other namespaces are rejected), likegit update-ref.<newvalue>/<oldvalue>resolve through the revspec grammar. With no<oldvalue>the write is unconditional; with one it is a compare-and-swap that fails unless the ref currently holds that value. In update mode an all-zero<oldvalue>requires the ref to be absent (git's create-only form).-ddeletes the ref (optionally verifying a concrete<oldvalue>first — an all-zero value is rejected, since a ref asserted absent has nothing to delete); deleting a branch refuses the currently checked-out one — a kit safety divergence (git's plumbing would, leaving HEAD dangling).kit ref list [--pattern <glob>]— print every ref's full name and resolved hash, one<refname> <hash>line per ref, sorted lexicographically by name. Coversrefs/heads/*,refs/tags/*, andrefs/remotes/*/*— the same read scope asshow-ref/for-each-ref. An empty repo (no commits yet) prints nothing and still exits 0.--patternfilters to full ref names matching a shell glob (*spans/,?matches one char,[...]is a character class — the same matcher asbranch --list/tag -l). This is the stable inspection surface for refs: scripts should use it instead ofls/caton.kit/refs/, which is not a supported interface.kit ref cat <name>— print the resolved hash for exactly one ref, followingHEAD's symbolic indirection (HEADis kit's only symbolic ref).<name>must be a fully-qualified ref name —refs/heads/<b>,refs/tags/<t>,refs/remotes/<r>/<b>— or the literalHEAD; these are exactly the namesref listprints, so the two commands round-trip. Errors (exit 1) if the ref does not exist, or if HEAD is symbolic but its target branch has no commit yet.
Attestations:
kit attest [--commit <hash>] [--algorithm <alg>] [--signer <kind>] [--predicate-type <uri>] [--predicate-file <path>] [--external-signer-arg <V>]... [--additional-signer "<spec>"]...— produce a signed DSSE attestation for a commit. The signed payload is an in-toto v1 Statement carrying the commit hash assubjectand the user-supplied predicate, wrapped in a DSSE envelope. On success prints the att-id (BLAKE3 over the canonical envelope, 64 hex) and stores the envelope under.kit/attestations/<commit>/<att-id>.dsse.Flags:
--commit <hash>— commit to attest. Defaults to HEAD.--algorithm <alg>—ed25519,secp256k1, orp256. Defaults toattest.default_algorithmin config, elseed25519.--signer <kind>—repo-key(default),keystore, orexternal. Picks the primary signer.--predicate-type <uri>— predicate-type URI written into the Statement. Defaults to the empty-predicate placeholder URI.--predicate-file <path>— JCS-canonical JSON object used as the predicate body. Omitted ⇒{}.--external-signer-arg <V>— repeatable argv token passed to the external-signer subprocess. If any instance is supplied the full list REPLACESattest.external_signer_argsfrom config.--additional-signer "<spec>"— repeatable; adds a second (or third, …) signature to the envelope.<spec>is a comma-separatedkey=valuelist:algorithm=<alg>,signer=<kind> [,path=<file-or-binary>][,args=<a>|<b>|<c>]. Heresigner=accepts onlyrepo-keyorexternal(notkeystore) — unlike the top-level--signer, which also takeskeystore. Signers run in the order given; the resulting{keyid, sig}tuples appear in that same order in the envelope. Any signer failure aborts the attestation — no partial envelopes are written.
Example — sign with two algorithms at once:
kit attest --algorithm ed25519 \ --additional-signer "algorithm=p256,signer=repo-key" \ --predicate-type https://example.com/review/v1 \ --predicate-file review.jsonkit verify-attest [--commit <hash>] [--trust-roots <path>] [--algorithm <alg>] [--format=json]— verify every attestation attached to a commit. For each envelope, looks each signature'skeyidup in the trust-roots registry and verifies the DSSE PAE. Reports per- signature verdicts to stderr; with--format=json, also emits one JSON object to stdout:{"ok":<bool>,"commit":"<64-hex>","error":"<string>|null", "attestations":[{"id":"<64-hex>|null","error":"<string>|null", "signatures":[{"keyid":"...","algorithm":"<string>|null","verified":<bool>, "reason":"<string>|null"}]}]}— the JSONsignatureslist is unfiltered (it always carries every signature, independent of--algorithm, which only filters the stderr report). Exits0iff every listed attestation has at least one verified signature,65(dataerr) if any failed,1(general_error) if the commit has no attestations.Flags:
--commit <hash>— commit to verify. Defaults to HEAD.--trust-roots <path>— path to a trust-roots TOML file ([[trust_root]]entries withkeyid,kind,pubkey_hex). Defaults to$XDG_CONFIG_HOME/kit/trust-roots.toml. kit refuses to verify against an in-repo path unless--trust-rootsis passed explicitly — without that gate, a hostile clone could ship its own trust-roots and have verification print "ok" against attacker keys.--algorithm <alg>— filter reported signatures by algorithm (ed25519,secp256k1,p256). Unmatched signatures are omitted from the report.
Example:
kit verify-attest --trust-roots ~/.config/kit/trust-roots.tomlkit trust add <keyid> <pubkey-hex> [--kind <kind>] [--trust-roots <path>] [--force]/kit trust list [--trust-roots <path>] [--json]/kit trust remove <keyid> [--trust-roots <path>] --yes— manage the trust-roots registrykit verify --trustedandkit verify-attestread (same[[trust_root]]TOML file, same default path).addvalidatespubkey-hexdecodes and, ifkeyidembeds a hex/digest body (the<algorithm>:<hex>orblake3:<hex>shapes), that it matches the given pubkey — a mismatch is rejected, not silently dropped.--kinddefaults toed25519(commit/remix/tag signing is Ed25519-only today);p256-sec1andsecp256k1only matter toverify-attest.addrefuses to overwrite an existing keyid without--force;removerequires--yesand fails if the keyid isn't registered.list --jsonemits a JSON array of{keyid, kind, pubkey_hex}.Example:
kit keygen --print-pubkey # ed25519:<hex> kit trust add ed25519:<hex> <hex> # trust your own key kit trust list kit verify --trusted HEAD
Branches / refs:
kit branch/kit branch <name>/kit branch -d <name>— list, create, or delete branches. The default list prints<marker> <name>with no commit id (likegit branch);*marks the current branch.-v/--verboseadds the abbreviated tip id and commit subject (<marker> <name> <short> <subject>, name column padded likegit branch -v); the abbreviated id is a BLAKE3 prefix, not a 40-hex SHA-1 prefix.--format=jsonon the list form emits JSONL with keysname,current,hash.kit branch -D <name>— force-delete. kit does not track per-branch merge status, so-Dbehaves like-dhere: both error on an absent branch (likegit branch -D <missing>) and both refuse the checked-out branch (deleting it would dangle HEAD).kit branch -m [<old>] <new>— rename a branch (the current branch when<old>is omitted). CAS-guarded: refuses to clobber an existing<new>, and moves HEAD when the renamed branch is checked out.kit branch --show-current— print the checked-out branch name (empty output on a detached HEAD), likegit branch --show-current.kit branch [--list] [--contains [<c>]] [--no-contains [<c>]] [--merged [<c>]] [--no-merged [<c>]] [<pattern>...]— filter the listing (likegit branch).<pattern>arguments are shell globs matched against branch names (*,?,[…];*spans/, sofeature/*works — git's non-pathnamewildmatch); a branch is kept if it matches any pattern. Patterns are only treated as filters in list mode — with--listor any ancestry filter present — sokit branch <name>still creates a branch.--contains [<c>]keeps only branches whose tip has<c>as an ancestor;--no-contains [<c>]inverts it.--merged [<c>]keeps only branches already merged into<c>(that is, whose tip is an ancestor of it);--no-merged [<c>]inverts it. All four ancestry commit args default toHEADwhen omitted (like git). Patterns and ancestry filters combine (a branch must satisfy all of them).--listis an explicit list selector — listing is already the default when no create/delete/rename flag is given, but--listalso enables<pattern>filtering (for example,kit branch --list "release/*", orkit branch --list mainto test existence).kit checkout <branch>— switch HEAD and restore files. Refuses to run when staged changes, dirty tracked files, or untracked path collisions would be overwritten. Non-colliding untracked files are preserved across the switch (git branch-switch semantics); tracked files the target tree drops are removed, with emptied directories pruned. PrintsSwitched to branch '<n>', orAlready on '<n>'for a no-op switch (which still runs the dirty-worktree guard).kit checkout -b|-B <new> [<start>]— create a branch at the start-point (default HEAD) and switch to it;-brefuses to clobber an existing branch,-Bcreate-or-resets. PrintsSwitched to a new branch '<new>'.kit switch <branch>/kit switch -c|-C <new> [<start>]— git's modern branch-switch UX; a thin front-end overcheckoutwith the same clobber guard and output.-ccreates (refuses to clobber),-Ccreate-or-resets, then switches. (switch -/@{-1}is a follow-up: kit does not yet track a previous branch.)kit clean [-n] [-f] [-d] [-x|-X] [<path>...]— remove untracked files from the worktree. Destructive, so it refuses to delete without an explicit-f(matching git'sclean.requireForcedefault);-npreviews withWould remove <path>lines. Without-d, untracked directories are left alone (git semantics);-drecurses into them and removes them — but, like git, an untracked directory is only removed wholesale (shown<dir>/) when nothing inside survives: ignored files are kept (unless-x) and keep their parent directory, and a nested repository (a subdirectory containing.kit/.git) is left untouched (git needs the double-force-ffto remove one, which kit doesn't offer).-xalso removes ignored files,-Xremoves only ignored (-xand-Xare mutually exclusive). Trailing pathspecs limit the scope to matching top-level entries and whole directories (.means everything under cwd); a pathspec naming a file inside an otherwise fully-removable untracked directory is a known limitation — name the directory or use.. Ignore matching uses the shared path-aware ignore matcher (see "Ignore files" below).kit tag [--format=json]— list, create, or delete tags.kit tag(no args) — list tags; annotated/signed tags are marked.kit tag -l [<pattern>]— list tags, optionally filtered by a shell glob (for example,kit tag -l 'v*'), reusingbranch'swildmatchengine.kit tag <name> [<commit>]— create a lightweight tag (a ref pointing straight at<commit>, default HEAD).kit tag -a <name> [-m <msg>] [<commit>]— create an annotated tag object (target, tagger identity, message, timestamp). Without-m,$EDITORis launched.kit tag -s <name> [-m <msg>] [<commit>]— create a signed annotated tag: an Ed25519 signature over the canonical tag bytes under the distinctkit.tagdomain. Verify withkit verify <name>.kit tag -d <name>— delete a tag.--author <spec>overrides the tagger identity (same grammar ascommit --author).--format=json— on the list form, JSONL with keysname,hash,annotated,signed(mirroringbranch --format=json); on create/delete, one{"ok":true,"kind":"…",...}outcome object.
kit merge [--no-commit] [-m <msg>] [--format=json] <branch> | --continue | --abort— three-way merge into HEAD. Fast-forwards and clean merges refuse to overwrite staged changes, dirty tracked files, or untracked path collisions. On conflict, the conflicting paths are materialized for resolution and a resumable state is recorded; see "Resolving conflicts" below.-m/--messageoverrides the merge commit message (defaultMerge branch '<name>').--no-commitperforms the merge and stops before committing: it stages the merged tree and recordsMERGE_HEAD, then leaves you to finish withkit commit(which records a two-parent merge commit) orkit merge --continue. A fast-forward creates no commit, so--no-commitdoes not affect it.--format=jsonemits one JSON object to stdout for every outcome:{"ok":true,"kind":"up-to-date"|"fast-forward"|"no-commit"|"merge-commit"|"aborted",...}on success, or{"ok":false,"kind":"conflict","conflicts":[<path>,...], "error":"..."}on a conflict pause (and{"ok":false,"error":"..."}for any other error).kit cherry-pick [-n|--no-commit] [-m|--mainline <parent-number>] [--format=json] <hash> | --continue | --abort— apply a commit to the current branch. Refuses to overwrite staged changes, dirty tracked files, or untracked path collisions. On conflict, records resumable state; see "Resolving conflicts" below.-n/--no-commitapplies a clean pick to the index plus worktree without creating a commit (runkit commitwhen ready; the result has the current branch as its single parent). A-npick that conflicts is refused before anything is written — kit cannot represent a staged-but-unresolved conflict, and a laterkit commitonly guards merges, so re-run without-nto use the resumable--continueflow.-m/--mainline <parent-number>selects which parent of a merge commit is the mainline (git semantics): it is required when replaying a merge (kit refuses to guess which side to diff against) and rejected for a non-merge commit. Note this differs fromgit commit -m— git'scherry-pick -mis mainline selection, not a message override.--format=jsonemits one JSON object to stdout, same shape asmerge("kind":"commit"|"no-commit"|"aborted"on success,"kind":"conflict"with aconflictsarray on a pause).kit revert <commit> | --continue | --abort[-n|--no-commit] [--format=json]— create a new commit that undoes<commit>(the inverse of cherry-pick: it applies the reverse of the target's diff). The commit message isRevert "<subject>"with aThis reverts commit <hash>.trailer. This is a forward commit — it does not rewrite history, so the reverted commit stays reachable (no gc/recovery interaction). Same safety guards and resumable conflict workflow as cherry-pick.-n/--no-commitstages the reverted tree (index plus worktree) without committing, so you can revert several commits and commit once. Reverting a merge commit is refused — the inverse depends on which parent is the mainline, and-m/mainline selection is not yet supported.--format=jsonemits the same outcome-object shape asmerge/cherry-pick.kit rebase [-i|--interactive] [--format=json] <revspec> | --continue | --abort | --skip— replay commits onto a different base. The target is resolved through the shared revspec resolver (branch, tag,HEAD~n, full/short hash). Restore steps refuse to overwrite staged changes, dirty tracked files, or untracked path collisions. On conflict the rebase pauses with resumable state;--skipdrops the current commit. See "Resolving conflicts" below.-i/--interactiveopens the todo list in$EDITORbefore any mutation. Each line is<command> <commit> <subject>, oldest-first (top is applied first). Reorder lines to reorder commits; delete a line (or usedrop/d) to drop a commit;reword/redits a commit's message when it is replayed;squash/sfolds a commit into the previous one and combines their messages (via the editor);fixup/ffolds in but keeps the previous message. Asquash/fixupcannot be the first line (nothing to fold into) and is rejected before any mutation. Removing every line resets the branch to the base. Areword/squashwhose replay hits a conflict still reopens the editor on--continue.edit(stop-to-amend) is not yet supported.--format=jsonemits one JSON object to stdout on the top-level outcomes:{"ok":true,"kind":"up-to-date"|"rebased"|"aborted",...}on success,{"ok":false,"kind":"conflict","conflicts":[<path>,...], "error":"..."}on a conflict pause. Best-effort on the interactive (-i) editing path and a handful of deep internal errors (for example, signer setup), which stay stderr-only.kit bisect start | good | bad | skip | reset | run <cmd> [args…]— binary search for a bug.rundrives the loop automatically: it checks out each candidate, runs<cmd>, and classifies from the exit status (git's contract:0=good,125=skip,1–127else=bad,≥128=abort), then restores the original HEAD and prints the first bad commit. The candidate hash is also exported asKIT_BISECT_COMMIT. (kit's bisect is otherwise print-candidate —start/good/bad/skipprint the next candidate rather than checking it out — sorunchecks out transiently for the test and parks nothing at the end.) Each candidate is checked out with--force, so a test command that scribbles on tracked files (aCargo.lockrefresh, a regenerated snapshot) doesn't block the next checkout. If every remaining candidate is skipped (exit125),runreports the result is ambiguous — like git's "the first bad commit could be any of …" — and exits non-zero rather than guessing. Caveat: in a sparse-checkout reporunmaterializes the full tree for each candidate (the transient checkout doesn't re-apply a persisted sparse cone).kit gc [-n|--dry-run] [--grace-secs <secs>]— reclaim unreachable objects (mark-and-sweep). Under the repo lock it expires stale recovery entries, computes the live set reachable from the retention roots (refs, stash, in-progress operations, attestations, and the recovery log of commits superseded bycommit --amend/reset/rebase), then deletes unreachable objects older than the grace window (default 14 days).-n/--dry-runreports what would be pruned without deleting.--grace-secs 0prunes every unreachable object, but bypasses the grace window — the grace window is gc's concurrency-safety net (some root-publishing paths such astagandfetchwrite an object before publishing the ref that makes it reachable), so--grace-secs 0must only be run when no other kit process is operating on the repo (gc prints a warning). Fail-closed: a missing/corrupt root, a malformed ref, or the reachability cap aborts the run with nothing deleted, and an object whose age can't be read is kept. Seedocs/specs/SPEC-GC.md.
Resolving conflicts
merge, cherry-pick, and rebase all share one resumable-conflict
workflow. A path where both sides changed the same regular file on
disjoint lines is auto-merged line-by-line (the union of both edits, no
markers) — changes on different lines merge cleanly even when adjacent. A
conflict is only raised when the two sides' changed regions overlap
(modify the same or interleaving lines), or for binary files. When a 3-way
merge cannot auto-resolve a path, the command:
- Applies the merge's clean (non-conflicting) changes to the index
and worktree (so, for example, files the operation adds/removes/modifies away
from the conflict are already staged), then materializes the
conflicting paths into the worktree (staging the ours-side blob so
each path is "resolvable"):
- text modify/modify (with overlapping line changes — disjoint
ones auto-merge) and add/add → classic 2-way Git markers
(
<<<<<<< ours/=======/>>>>>>> theirs) are written into the file. - binary, symlink / executable-mode, delete/modify, and file-vs-directory → no markers (they would corrupt the file); the surviving side's content is left in place. Resolve these by hand. Each prints a per-path note.
- text modify/modify (with overlapping line changes — disjoint
ones auto-merge) and add/add → classic 2-way Git markers
(
- Records resumable operation state under
.kit/(see below) and exits non-zero.
To finish, for each conflicting path: edit the worktree file to its
resolved content (remove all conflict markers), kit add <path>, then:
kit merge --continue # or cherry-pick --continue / rebase --continue
--continue refuses while any text-marker file still contains markers,
and refuses whenever a conflicted path's worktree resolution does not
match what is staged in the index — so an unstaged resolution can't be
silently dropped. This covers every shape: an edited regular or
executable file, a path deleted without kit rm, and a file replaced
by a symlink or directory without kit add. An unchanged conflict
(the worktree still equals the auto-staged ours-side, including its
executable / symlink mode) needs no re-add and continues. The committed
tree is built from the resolved index/worktree — not the
conflict-time "ours wins" snapshot — so your edits (including a third
distinct resolution) and the merge's clean changes both land.
Alternatively:
kit merge --abort # restore HEAD, branch ref, index, and worktree
kit rebase --skip # rebase only: drop the current commit, keep going
--abort restores the pre-operation state (or fails with a clear,
recoverable error and changes nothing). Starting a new merge / cherry-pick
/ rebase while one is already in progress is refused.
Operation-state files
All live under .kit/; rebase keeps its state inside
.kit/rebase-apply/. These use Git-compatible names plus one documented
kit sidecar. The .kit/index stays a single-stage resolved staging
area — there are no unmerged index stages (SPEC-INDEX is unchanged).
| File | Meaning |
|---|---|
MERGE_HEAD |
other parent of an in-progress merge (presence ⇒ merge) |
CHERRY_PICK_HEAD |
commit being applied by an in-progress cherry-pick |
ORIG_HEAD |
HEAD before the operation, used by --abort |
MERGE_MSG / CHERRY_PICK_MSG |
pending commit message |
kit-conflicts |
kit sidecar: one line per conflicting path with the conflict kind and base/ours/theirs blob hashes |
rebase-apply/ |
rebase state (head-name, orig-head, onto, todo, actions, done) plus a kit-conflicts sidecar when paused. actions is parallel to todo (pick/reword/squash/fixup, from rebase -i) |
Remote / sync:
kit remote [-v|--verbose] [--format=json]— list the configured remotes. Bareremoteprints one remote name per line (git-shaped);-v/--verboseadds the URL and direction (<name>\t<url> (fetch)/(push)).--format=jsonemits the remote configuration as JSON; an unset remote produces empty stdout.kit remote add [<name>] <url>— add a remote. With a<name>, registers a named remote (remote.<name>.url); without one, sets the flat defaultremote_endpoint. The URL MUST start withkit+<scheme>://(see below).kit remote set [<name>] <url>— alias forkit remote add.kit remote remove <name>(aliasrm) — delete a named remote. The reserved namedefaultclears the flatremote_endpoint.kit remote rename <old> <new>(aliasmv) — rename a named remote and repoint anybranch.<b>.remoteupstream tracking it. Refuses to clobber an existing<new>. Removing or renaming a remote never touches the user-scopedtrusted_remote_endpoint, which is keyed by exact URL rather than remote name (so the credential-trust gate is preserved).kit remote get-url <name>— print a remote's URL (usedefaultfor the flat default remote).kit remote set-url <name> <url>— change an existing remote's URL (validated likeremote add).kit clone [--depth N] [--sparse ...] [-b <branch>] [-o <name>] [--no-verify-signatures] [-q|--quiet] <url> [<dir>]— clone a repository into<dir>(defaults to the final URL path segment).--sparse <pattern>...persists the patterns and materializes only the matching files via the verifiable sparse-checkout pipeline (requires a build with--features sparse-checkout). Note this is an enhancement over git: kit's--sparsetakes one or more PATTERN arguments, whereas git'sclone --sparseis a boolean flag (it cones to the top-level files and you add patterns afterward).--depth Nis parsed but not yet wired: passing it fails with a clear--depth is not yet wiredusage error rather than silently producing a full clone; see "Divergences from Git".-b/--branch <name>— check out<name>instead of the remote's default branch. The branch MUST exist among the remote's advertised refs; unlike the default heuristic (current default branch, falling back to whatever the remote advertises first) an explicit-bnever silently substitutes another branch — a missing branch fails the clone with a "remote branch not found" error.-o/--origin <name>— name the cloned remote<name>in the new repo's.kit/configinstead of the implicit flatdefaultremote (mirrorskit remote add <name> <url>at clone time). The name must be dot-free and ref-safe, like a namedremote add. Tracking refs land underrefs/remotes/<name>/*to match.
kit fetch [<remote>] [--all] [--no-verify-signatures] [--format=json] [-q|--quiet]— download from remote without merging. Fetched branch tips are stored underrefs/remotes/<remote>/<branch>(refs/remotes/default/<branch>for the flat default remote) and do not move local branches.--allfetches every configured remote (the flat default plus every namedremote.<name>.url) in one invocation, printing each remote's usualFrom <url>plus per-ref summary in turn; mutually exclusive with an explicit<remote>argument. A per-remote failure does not stop the others —fetch --allcontinues through the full list and exits non-zero if any remote failed.--format=jsonemits one JSON object per remote fetched to stdout:{"ok":true,"remote":"...","endpoint":"...", "updated":[{"name":"...","old":"<hex>|null","new":"<hex>"}]}on success, or{"ok":false,"error":"..."}on failure.kit pull [<remote>] [--all] [--no-verify-signatures] [--format=json] [-q|--quiet]— fetch, then fast-forward the current branch fromrefs/remotes/<remote>/<branch>. Divergent histories are refused; use explicit merge/rebase flows after resolving the divergence. Fresh repos with no local branch tip initialize the current branch/worktree from the remote default branch.--allpulls from every configured remote in turn (same enumeration and failure-tolerance asfetch --all), fast-forwarding the current branch from each; mutually exclusive with an explicit<remote>argument.--format=jsonemits one JSON object per remote pulled:{"ok":true,"remote":"...","endpoint":"...", "branch":"...","old":"<hex>|null","new":"<hex>|null", "up_to_date":<bool>}on success, or{"ok":false,"error":"..."}on failure.Post-fetch signature verification (default ON).
clone/pull/fetchverify every commit/remix/tag the fetch newly introduces — the same checkkit verify <rev>runs manually (kit_core::sign::{verify_commit,verify_remix,verify_tag}) — before publishing the remote-tracking ref. A structurally invalid or missing signature aborts with exit 65 (DATAERR) and leaves local branch/ remote-tracking refs and the working tree untouched. Only the objects a given fetch actually introduces are checked, not the whole history (bounded cost per fetch). Opt out per invocation with--no-verify-signatures, or persistently for scripted/CI use via the user-scopedpull.require_signed = falseconfig key — it cannot be set from a cloned repo's own.kit/config(see THREAT-MODEL.md §4).--allapplies the same verification (and the same opt-out) to every remote it iterates.Library.
clone,fetchandpullrun on thekit-synccrate (rust/crates/kit-sync/README.md), which an embedder can call directly.kit push [<remote>] [--all] [--force|--force-with-lease] [--dry-run] [--format=json] [-q|--quiet]— push refs and packs to a remote. With no<remote>, pushes the current branch to its upstream (thebranch.<b>.remote/.mergetracking pair), falling back to the configured default remote; an explicit<remote>name overrides that choice. The default push is CAS-protected: a non-fast-forward is rejected using the remote-tracking ref as the lease.--allmirrors everyrefs/heads/*(also CAS-safe).--forceoverwrites the remote branch unconditionally (skips CAS);--force-with-leaseoverwrites only if the remote hasn't moved past the last-seen tip (the two are mutually exclusive);-fis an alias of--force.-u/--set-upstreamrecords the pushed remote as the branch's upstream even if one was already set.--dry-runresolves the push plan without contacting the remote. Every endpoint flows through the per-endpoint credential-trust gate, which is keyed on the resolved endpoint URL, never the remote name. On successpushprints git'sTo <url>plus ref-update summary (orEverything up-to-date).--format=jsonemits one JSON object to stdout:{"ok":true,"remote":"...","endpoint":"...","branch":"...", "remote_branch":"...","old":"<hex>|null","new":"<hex>","forced":<bool>, "up_to_date":<bool>,"steps":<n>}on success (stepsis 0 when up to date, otherwise the number of branch advances the push took;--allreports the total plusref_count), or{"ok":false,"rejected":true, "branch":"...","error":"..."}on a non-fast-forward (CAS) rejection ({"ok":false,"error":"..."}for any other failure). On a V2 Connect remote, pushes at or above its advertised upload threshold open a signed ticket for each data pack and packmap node, then consume those tickets with the branch advance. A single advance can carry six data packs. A push that needs more is split automatically along the branch's first-parent history into several advances, each moving the branch to an intermediate commit whose closure fits; nothing is uploaded ahead of its advance. Every intermediate state is published: the remote branch, its server hooks, receipts and lifecycle events all fire once per advance, and a failed push leaves the branch at the last published commit, which the error names. Runkit pushagain to resume from there (the remote-tracking ref follows each advance). The first advance uses your usual lease (or--force); every later one is a compare-and-swap on the previous advance's commit, even under--force. Before uploading anything, the client checks that your stored write grants cover every advance: a non-owner key needscto create the branch (oru/fto update it), and anfgrant for the later advances, on every server until indexed-modeu-only fast-forwards land; the owner key needs no grant. A push that would be refused midway is refused up front, with no advance published. A single commit or merge that cannot be split (a merge always lands whole) and still needs more than six packs is refused before any upload, naming the commit; ask the operator to raisemax_pack_bytes. On a terminal the progress line readsWriting objects (step 2/5); piped, apushed step 2/5: branch now at <hex>line follows each advance;--quietprints neither. Large packs upload in parts. Receipt files in the repository's commonupload-parts/cache let the nextkit pushresume the parts already accepted, provided the regenerated push plan is identical. After an interruption, runkit pushagain to resume; the message reports how many parts were saved.Transfer progress.
clone/push/pull/fetchstream a live, honest progress line on stderr while the network transfer runs —Writing objects: N objects, B byteswhile building/uploading the outgoing pack,Unpacking objects: N objectswhile applying a downloaded one. The counts are real (objects actually staged/unpacked, bytes actually handed to the transport); kit never fabricates git'sEnumerating/Counting/Compressing objectsorTotal N (delta D)lines — kit's transport is one-object-per-pack and computes no cross-branch delta graph. Progress shows only when stderr is a tty;-q/--quietforces it off, andKIT_PROGRESS=always/neveroverrides the tty-detection explicitly (mirrorsNO_COLOR/CLICOLOR_FORCE).kit serve [--idle-timeout-secs <secs>] [--max-session-secs <secs>] <path>— internal SSH transport server. It speaks the kit-rpc SSH framing on stdin and stdout and has no other mode. It has no network listener. The engine is thekit-servecrate. The refs and packs are the files thatkit+file://remotes use.kit serve --root <dir> [--principal <hex>] [<ns>/<name>]is the multi-repository form that a forge forces. The path names a<NAMESPACE>/<NAME>under<dir>. When the path is omitted, anSSH_ORIGINAL_COMMANDof exactlykit serve <path>supplies it. A bare name, an uppercase byte,.., an extra component or a symlinked component is refused. Only the Ed25519 owner of the namespace may write.--principalis a raw 32-byte key as 64 lowercase hex characters. It is the sshd configuration's assertion of the caller's key. The command reads it from argv alone, never from the environment. A session without it is read-only in root mode. A namespace that is an address has no key to match over ssh, so it is read-only. Plain mode, without--root, lets every session write. See SPEC-TRANSPORT §4.1 and SSH-SECURITY.md §5. Both forms hold a sharedserve.lockin<path>/.kitfor as long as the process is alive. Any number of concurrentserveprocesses may hold it at once. A local worktree-mutating command orgcrun against that same path while the lock is held prints a warning to stderr and proceeds. It is not refused. This is detection, not coordination (SPEC-CONCURRENCY §3.1).--idle-timeout-secs <secs>(default60;0disables it) ends the session after that long without a byte from the client. It applies before the handshake, between requests and in the middle of an upload. The server discards the partial pack, answersError{INVALID_REQUEST, "idle timeout"}and exits 76. An upload that keeps sending never trips the timeout, however slow it is. The timeout bounds only a silent client. A client that trickles bytes, or stops reading a download, is not idle. For those,--max-session-secs <secs>(default0, off) ends the process that long after it starts, whatever the client does, with exit 76. Set it above your longest legitimate clone or push. Both flags accept at most 604800 (7 days). See SSH-SECURITY.md §4.The server serves refs only under
refs/(SPEC-REFS §2). It refuses a read or write of any other name by name ("ref name must start with refs/ …"). At startup, when no otherkit serveprocess serves the root, it removes upload temp files (packs/.<hex>.tmp.<pid>.<seq>) that a crashed server left, once they are an hour old. It refuses a root marked.kit/server-meta(exit 78), because the refs of that root live in a database.--httpand--listen-encdo not exist.KIT_API_TOKENis sent as a bearer token bykit+https://remotes.kit closure export <revision> [--history] [-o DIR] [--force] [--format=json]— write a full-disclosure closure of<revision>asDIR/MANIFEST.mkclplusDIR/<pack_key hex>.packfiles (raw-only v1 packs). Default mode is snapshot (the commit and its tree closure; parents referenced but not included);--historyexports the full ancestry. DefaultDIRis./<short id>.closure/. Refuses to write into a non-empty directory without--force. This is notkit git export, which publishes refs to a git mirror;closure exportis kit's native object-set disclosure (nearest git relative:git bundle). Status line:closure: <mode>, <n> objects in <k> pack(s), <total> B -> DIR. JSON:{root, mode, packs: [{file, blake3, bytes}], objects, manifest}.kit closure verify <commit-id> [--from DIR] [--history] [--show-unreferenced] [--format=json]— verify a closure. With--from DIR, readsMANIFEST.mkcland every pack it names and checks them against a trusted 64-hex<commit-id>(not a revision). Without--from, checks the local repository (<commit-id>may be a revision;--historyselects history vs snapshot); a corrupt on-disk object (its bytes no longer hash to its filename) is reported undercorrupt, the same as a corrupt object in a served pack — localclosure verifyis an fsck-shaped check, not a hard read failure. Output:ok: closure complete (<verified> objects, <mode>)orbad: closure incomplete: <m> missing, <c> corruptplus up to 10 ids each;unreferencedis a note, not a failure, and — local mode only — is hidden unless--show-unreferencedis passed (the local store is expected to be a superset of any single commit, so a long unreferenced list is usually noise;--fromalways shows it). JSON has these fields (root, mode, verified, complete, missing[], corrupt[{id, reason}], unreferenced[], unreferenced_checked);unreferencedis[]in local mode unless--show-unreferencedis passed, and the unchecked streaming mode prints a note explaining how to opt in. Incomplete closures exit65(DATAERR).The store-less
--from/pack mode can check unreferenced objects, while local mode streams only reachable objects by default and enumerates the whole store only with--show-unreferenced.kit git export <dest> [--remote-name <name>] [--ref <ref>]... [--no-attest] [--algorithm <alg>] [--signer <kind>] [--passthrough] [--json]— deterministic one-way export of branches and tags to a git mirror (SPEC-GIT-BRIDGE).<dest>is a git URL or a local path (a missing/empty local path is initialized bare);--remote-namedefaults tomirror, and--reftakes full names (refs/heads/.../refs/tags/...). Translation is byte-deterministic: the same history yields the same git SHA-1s on every machine, and kit-only fields (signer, signature, identity, annotation slots) ride inkit-*commit headers so the original signed objects are reconstructible. Refs a conformant bridge cannot represent (git-illegal names, remix ancestry, non-canonical chunking) are skipped per-ref with a warning; an export where everything was skipped exits non-zero. Pushes use per-ref--force-with-leasefrom recorded state under.kit/git/<name>/; kit-side history rewrites mirror as force-pushes. Unless--no-attest, each exported head gets a signedgit-bridge/v1DSSE attestation (saved likekit attest, published on the mirror'srefs/kit/attestationsref — fetch it with an explicit refspec; git clones skip it by default). Attestation signing uses the same signer resolution askit attest(--algorithm/--signerflags, else config defaults), so a signing key must exist unless--no-attest. Branch deletion never propagates (export is add/update-only), and each plain-export--remote-namestate dir is bound to one destination and one direction (fork-mode state dirs lease against a fresh observation per push and are not destination-bound); bidirectional sync does not exist.kit git import <url> [<dir>] [--remote-name <name>] [--key <path>] [--json]— import a git upstream as an importer-signed downstream fork (SPEC-GIT-IMPORT). With<dir>: init a fresh kit repo, import all branches/tags, and check out the upstream default branch. Without: add the upstream to the current repo asrefs/remotes/<name>/*tracking refs plus tags. Every translated commit/tag is signed by a DEDICATED import key (.kit/keys/git-import.key, generated on first use and pinned per state dir — collaborative tracking requires sharing it; a different key produces an unrelated fork). Original authorship rides in the author identity; original git bytes are retained under.kit/git/<name>/with agit-import/v1attestation per head. Unrepresentable refs skip per-ref with warnings (submodules, kit-illegal names, historic-mode trees in fork state dirs). Upstream history imports once per (key, upstream); hashes are a function of the import key by design. Plaingit exporttoward an imported-from upstream is refused (the origin guard); export to new mirrors works normally. Expect total disk ≈ 2–3× the upstream.git(staging mirror plus translated store).kit git fetch [--remote-name <name>] [--key <path>] [--json]— fetch new upstream commits into the tracking refs and imported tags (a force-pushed upstream moves tracking refs with a loud warning; a tag you moved locally is never clobbered — fetch warns and leaves it).--keypoints at the import signing key (default: the pinned key file), and--jsonemits machine-readable JSON on stdout.kit git pull [--remote-name <name>] [--key <path>] [--json]additionally fast-forwards the current branch; divergence refuses with the executable hint (kit merge <name>/<branch>— imported history is ordinary kit history, integrated natively).kit git export --passthrough --remote-name <import-name> <fork-url>— fork mode (SPEC-GIT-BRIDGE §14): publish an imported repo as a true git fork. Imported objects re-emit their ORIGINAL sha1s; only native commits on top translate, so the pushed branch sits directly on the upstream commits and PRs/merge-bases work. Upgrades the import state to directionfork(sticky); plain export under that name is then refused, whilegit fetch/pullkeep working.kit git verify [--remote-name <name>] [--fork-audit] [--ref <ref>]...— audit recorded bridge state against the local store: bridge-translated objects shallow-verify (SPEC-GIT-BRIDGE §10) and must reconstruct to their mapped twin; imported objects must have retained raw bytes hashing to their sha1 and a twin signed by the pinned importer key.--fork-audit(§14.3) additionally re-derives every tree/blob referenced by bridge commits from its kit twin and requires the exact sha1. Exits non-zero listing each failing object; unsigned (all-zero-signature) objects also fail, reported as a count ("unsigned", never "tampered" — SPEC-GIT-BRIDGE §10).kit git status— one block per.kit/git/<name>/state dir: direction, recorded source/dest, pinned importer key, tracking refs (import) and lease positions (export), staging health.kit git format-patch <A..B> [-o <dir>] [--stdout]— render native commits asgit am-able mbox patches (oldest first, merges skipped) — the contribution path that needs no writable fork. A single rev means<rev>..HEAD; for example,kit git format-patch upstream/main -o patches/.The
git-bridgefeature, on by default, provides thekit gitsubcommands. They shell out togit.
Agent integration:
kit mcp [--repository <path>]— start a Model Context Protocol server on stdio so MCP clients can drive local kit repositories through structured tool calls instead of raw shell. Exposes a conservative tool surface: status / diff (unstaged, staged, vs target) / log / show / branch / cat-file inspection, staging (add, unstage), signedcommit, branch create/checkout,init,keygen(ed25519 commit key, or secp256k1/p256 attestation keys), plus the differentiators —verify(signature check),attest(DSSE; supportsalgorithmandrepo-key/keystoresigner), andverify-attest(trust-roots-gated, with analgorithmfilter). Every tool takes an explicitrepo_path;--repository <path>confines all calls (symlink resolved) to that root. The server runs no network operations (push/pull/fetch/clone are not exposed), performs no history surgery (merge/rebase/cherry-pick are not exposed), and never passes-f/--force— kit's data-loss guards stay in force, and a "refuses without -f" error is surfaced to the agent verbatim. Additional guardrails specific to the agent threat model: a predicate file passed toattestmust resolve inside the repo (no reading outside files into a signed attestation), atrust_rootspath passed toverify-attestmust resolve outside the repo (a hostile clone's in-repo.kit/attest-trust-roots.tomlcan never be selected via the MCP — see THREAT-MODEL §"Trust-roots scope"), and the external/multi-signer attestation controls are intentionally omitted because they can direct subprocess execution (use theattestCLI for that flow).initializemust precede tool calls. Tool results carry the child command's output and its sysexits code on failure. Set the MCP client's server command to:kit mcp --repository /path/to/repoA build with the
mcp-v2feature also serves streamable HTTP with--http <addr>. The listener refuses to start without a bearer token. Put the token in a file and pass--token-file <path>. The file must hold only the token, must have at most 4096 bytes, and must not be readable by group or others. You can setKIT_MCP_TOKENinstead. Setting both is an error. No option takes the token on the command line, because other users can read a command line.--unsafe-allow-any-http-peerturns the check off. Use it only for local development.
Grants, epochs and repository visibility (SPEC-WRITE-GRANTS):
A grant is a statement signed by a namespace owner that lets a grantee's key
read or write repositories in that namespace on a deployment. kit grant
manages the grants you hold; the store is a directory of files under the
user config directory. It only holds client-side grants; it does not
register grants on a server.
kit grant create --cap CAP --grantee HEX (--repo NAME | --all) [--refs PATTERN=FLAGS]... [--audience ORIGIN]... [--ttl DURATION] [--epoch N | --offline] [--namespace NS] [--remote REMOTE] [--store]— create an owner-signed grant and print its header (give it to the grantee).CAPisread,read,writeorwrite(write,readis accepted and spelled canonically).--refsispattern=flagswith flags fromcufd, required with write and not allowed with read (a server without indexed mode cannot check a fast-forward, so it applies an update to an existing ref only underf: usecuf, notcu, for pushes that move a branch); audiences, ref scopes and flags are sorted and deduplicated.--ttlis at most30d(default7d). The epoch defaults to the remote's current one (GetGrantEpoch) or0with--offline; the audience defaults to the trusted remote's origin.--storealso adds it to your own store.kit grant add [--remote REMOTE | --offline] <file|->— verify a grant header's owner signature and add it to your grant store. Idempotent on the same grant id. Anything the verifier rejects is refused with the rule named. It then makes one short attempt to ask the trusted (or--remote) deployment for the namespace's epoch and warns when the grant's epoch is above it (see the epoch rule below); if that check fails it warns and stores anyway, and--offlineskips it.kit grant list [--check] [--remote REMOTE] [--json]— id, namespace, scope, capabilities, ref scopes, audiences, epoch, expiry and status (valid,expired,not yet valid).--checkasks the trusted (or named) remote for each namespace's epoch and marksstale epoch/future epoch, only for grants whose audiences include that remote (others areunchecked). It never contacts an origin that a grant file merely names.kit grant revoke <remote> [--namespace NS] [--audience ORIGIN]... [--prune] [--timeout DURATION]— sugar forkit epoch bump --by 1. It first lists the local grants the bump invalidates, and afterwards prints how to reissue. A grant that also lists an audience the bump does not cover stays valid there: it is listed as "still valid at ..." and kept.--prunedeletes only the grants every audience of which the bump covers, once it succeeds.kit epoch show <remote> [--namespace NS] [--json]— the epoch the remote stores for a namespace (0 if never set).kit epoch bump <remote> [--by N] [--namespace NS] [--audience ORIGIN]... [--timeout DURATION] [--json]— raise the epoch byN(1 to 1024, default 1), revoking every grant issued at a lower epoch. The statement is signed once. While the server answersunavailablewithRetry-After,bumpwaits (1 to 60 s per wait) and re-sends the identical statement, up to--timeout(default5m). Ctrl-C cancels and says the bump may still finish on the server; checkkit epoch showbefore running it again, because a new run signs a new statement and would raise the epoch again.kit visibility set <remote> public|private [--statement] [--audience ORIGIN]... [--timeout DURATION]— switch a repository between public and private. By default it is a signed request with the repository signing key (transport_auth = envelope, a trusted remote, and the owner's key). With--statementit sends an owner-signedkit-repo-visibility:v1statement with no envelope, which any owner scheme can sign.
Cloning a private repository: a Connect clone with transport_auth = envelope
signs with signing_key, which defaults to the repository-relative
.kit/keys/default.key. A fresh clone has no such file, so set a user-level
signing_key (or signer = keystore) before cloning.
Signing flags (grant create, epoch bump, grant revoke, visibility set --statement): the owner signs with the kit signing key (ed25519, the
default; the namespace is ed25519-<pubkey>) or, with --scheme secp256k1-eip191, with a software-keystore secp256k1 key (key.secp256k1_ref;
the namespace is its 0x address). A wallet or authenticator signs by import:
--print-statement writes the exact statement bytes to stdout (the digest or
challenge to sign goes to stderr); then rerun with --statement-file <that file> and either --signature <hex> (65 bytes r‖s‖v, v of 0, 1, 27 or 28,
a high s is normalized) or --webauthn-assertion <file> (JSON with base64url
publicKey (x‖y), authenticatorData, clientDataJSON and signature, DER or
low-s raw). A webauthn-p256 import is refused unless the user config pins the
relying party with grant.webauthn_rp = <rp_id> <origin>.... Every header is
verified locally before it is stored or sent.
How a held grant is used: kit push, pull, fetch and clone attach the best
grant from your store to each signed request, for the key that signs them. Among
the grants valid for the request, the higher epoch wins, then the most
specific repository and ref scope, then the latest expiry, then the greater
header bytes. A grant pre-issued for epoch e+1 therefore outranks a live
epoch-e grant until the owner raises the epoch. A write-only grant never
implies read on a private repository (only GetReceipt accepts it). Each signed
read may prompt for a signature when your key needs a touch or a passphrase.
Epochs are per deployment, so a grant lists the audiences it is valid at.
The grant store directory is never followed through a symlink: if
$XDG_CONFIG_HOME/kit/grants is a symlink, kit refuses it rather than trust
where it points. Files are read without following symlinks and only files named
<64 hex>.grant are considered.
Config / keys / version:
kit keygen— generate a new Ed25519 signing keypair.kit key generate [--backend B] [--label L] [--algorithm ALG]— generate a signing key in a keystore backend.ALGised25519,secp256k1, orp256; default ised25519.kit key list [--backend B] [--json]— list keys visible to a backend.kit key import --algorithm ALG (--hex HEX | --file PATH) [--backend B] [--label L]— import 32 bytes of signing key material where the backend allows import.kit key export [--backend B] [--label L] [--algorithm ALG] --unsafe-print-secret— export extractable key material. Non-extractable backends fail closed.kit key delete [--backend B] [--label L] [--algorithm ALG] --yes— delete exactly one backend key where deletion is supported.kit config [--format=json]— show all configuration values.--format=jsonemits a flat JSON object with every known key.kit config <key> [--format=json]— show one value.kit config --unset <key>— remove a configuration value. Deletes from whichever scope asetof that key would use — the repo layer (<repo>/.kit/config) for a repo-safe key, or the user-scoped layer ($XDG_CONFIG_HOME/kit/config) for aREPO_FORBIDDEN_KEYSkey — unless overridden by--local/--global. Idempotent: unsetting an already-absent key is a silent success (likerm -f), not an error; only an unknown key name is rejected. Takes no positional arguments.kit config [--local|--global] <key> <value>— set a configuration value, or--unset <key>to remove one.--localforces the repo-scoped layer (refused for aREPO_FORBIDDEN_KEYSkey, which can never live in a clone-traveling repo config);--globalforces the user-scoped layer even for an otherwise repo-safe key. Put--local/--globalbefore--unset(kit config --global --unset <key>, not--unset --global <key>) — like any clap value-taking option,--unsetconsumes the very next token as<KEY>, so a flag placed between--unsetand the key is swallowed as (invalid) key text rather than parsed separately. The two flags are mutually exclusive; neither is needed for the common case, where scope is chosen automatically by whether the key is inREPO_FORBIDDEN_KEYS.kit config <key> <value>— set a configuration value. Config section and variable names are matched case-insensitively (canonicalized to lowercase), like git — souser.Name,USER.EMAIL, etc. resolve to the same key as their lowercase form, in both this command and the config-file parser. Subsection names — the middle segment ofremote.<name>.urlorbranch.<branch>.remote— stay case-sensitive (git semantics), so a remote added asOriginis not silently folded toorigin.kit self update [--version <tag>] [--check] [--allow-downgrade] [--format human|json]— update this binary in place from a GitHub Release. The update fails closed. It runscosign verify-blobon the archive with its.cosign.bundleasset. The bundle must come from the release workflow of this repository at the exact tag being installed. A signature for another tag fails. It then checks the.sha256asset. A release that lacks either asset is refused.cosignmust be onPATH. No option skips these checks. The command runs no downloaded code before both checks pass. Only installer-managed binaries are updated: the.kit-installed-tagreceipt written byinstall.shmust sit next to the (canonicalized) executable, and the global$KIT_STATE_DIR/installed-tag(default~/.local/state/kit) must agree with it when present. Homebrew/cargo installs are refused with channel-specific guidance (brew upgrade kit,cargo install --locked kit-bin). Both receipts are rewritten after a successful swap, in the installer's exact format, so installer and updater stay interchangeable.--check— only report whether an update is available (works for any install method; compares against the running binary's own version); exits 0 either way, and--format jsoncarries the machine-readable bit (status: up-to-date | update-available).- Downgrade policy mirrors
install.sh: resolvinglatestnever downgrades; an explicit--versionpin may downgrade only with--allow-downgrade, loudly. - The staged binary must pass a pre-swap self-check (
versionmust emit exactlykit <X.Y.Z>); the swap is a same-directory atomic rename. Group-/world-writable install dirs are refused (installer parity). - Environment:
GH_TOKEN/GITHUB_TOKEN(API bearer; optional, raises the unauthenticated GitHub API rate limit),KIT_STATE_DIR,KIT_SELF_UPDATE_API_BASE(test/mirror override). - There is no background update check — the command only ever acts when invoked.
kit version— print the version. Emits exactlykit <X.Y.Z>\n. The top-levelkit --version/kit -Vflags are aliases of this subcommand and emit the identical string. Note this iskit <X.Y.Z>, not Git'sgit version <X.Y.Z>form — an intentional, documented divergence (the string is pinned by packaging asserts).
Ignore files
kit reads ignore patterns from both .gitignore and .kitignore at
the repository root. .gitignore is applied first and .kitignore last, so
under gitignore's last-match-wins rule a repo's own .kitignore can override
(including re-include via !) anything .gitignore set. Matching is honored
by add, clean, ls-files --others, restore/checkout, and the
worktree tree-builder.
Matching is path-relative and follows the gitignore semantics for this v1 subset:
- A pattern with no
/(other than a trailing one) matches at any depth (*.logmatchesa/b/c.log). A pattern containing a/— including a leading/— is anchored to the repo root (/foomatches only the top-levelfoo;src/genonly that path). *and?match within a single path segment;[abc],[a-z], and[!abc]character classes are supported;\escapes the next character.**as a whole segment crosses/: leading**/and middle/**/match zero or more directories, and a trailing/**matches everything inside a directory.- A trailing
/restricts a pattern to directories. A leading!negates (last match wins);\#/\!escape a leading#/!to a literal. Trailing unescaped spaces are trimmed. .kitand.gitare always ignored (matched on the basename, ASCII case-insensitively).
Deferred (documented non-goals for now): nested per-directory
ignore files (only the repo root is read), global excludes
(core.excludesFile), and escaped trailing spaces.
Divergences from Git
kit's local commands intentionally diverge from Git in a few places. These are documented behaviors, not bugs:
- Human output goes to stderr; stdout is reserved for porcelain/data.
kit's command summaries (
commit/merge/push/status…) are shaped like git's but are written to stderr, sokit statusand friends stay empty-on-clean in a pipeline and stdout carries only machine output (--porcelain,rev-parse,rev-list, …). git puts some of these on stdout; kit keeps its stderr convention. - Object ids in human output are 64-hex BLAKE3 prefixes, not git's
40-hex SHA-1 — the inherent hash-length divergence. Output otherwise
matches git's shape (for example,
To <url>plus<old>..<new> main -> main,[main <hash>] <subject>plus diffstat,Fast-forward). - git's object-count / delta-compression progress lines
(
Enumerating/Counting/Compressing objects,Total N (delta D)) are not emitted: kit's transport is one-object-per-pack and computes no cross-branch delta graph, so those specific numbers would be fabricated.clone/push/pull/fetchdo stream an honestWriting objects: N objects, B bytes/Unpacking objects: N objectsprogress line — real counts only; see "Transfer progress" above. kit remotelists remote names only (git-shaped); usekit remote -vfor<name>\t<url> (fetch)/(push).kit log --author/--grepare plain substring matches, not regexes. git's--author/--grepdefault to POSIX extended-regex matching against a free-textName <email>header; kit identities are opaque (Ed25519 keys,mid:Nnumbers, DID keys), so a regex engine would buy little for the common case. A literal substring (the typical--author=me/--grep=fix:usage) behaves the same either way.kit log --since/--untiluse a small, explicit date grammar, not git'sapproxidate. Accepted forms:@<unix-seconds>,now/today/yesterday,<N> <unit> ago,YYYY-MM-DD, andYYYY-MM-DD HH:MM:SS/YYYY-MM-DDTHH:MM:SS[Z](UTC). git's fuzzier natural-language forms (last Tuesday,3am,noon) are not accepted.kit status --porcelain=v2matches git's format except that object ids are full 64-hex BLAKE3 (git's are 40-hex SHA-1). Exact renames emit a2record (R100);--branchheader lines are not emitted.kit clone --depth Nis parsed but not yet wired. The flag is accepted by the parser butclonerejects it with a--depth is not yet wiredusage error rather than silently producing a full clone. Shallow-clone history truncation is a follow-up.kit blame --ignore-rev-precise(opt-in) is a content-addressed refinement of--ignore-revfall-through, not git's positional guess. git's--ignore-revpairs a changed hunk's k-th added line with its k-th removed line purely by position; kit hashes line content, so it can identify a fallen-through line's true surviving origin across a reformat/reorder — including cases the positional pass leaves credited to the noise commit as a "genuine insertion" because its own hunk has no local counterpart. The default--ignore-revfall-through is unchanged and stays byte-identical to git;--ignore-rev-precisemust be passed explicitly (and requires--ignore-rev/--ignore-revs-file).kit reset [--soft|--mixed|--hard] [-f] [<commit>]—--softmoves HEAD only;--mixed(the default) also resets the index; both leave the worktree untouched.--hardadditionally resets the worktree to the target tree — overwriting tracked-file changes and removing tracked files the target drops. Untracked files are kept (like git), except an untracked path that collides with one the target writes. As a safety divergence,--hardrefuses (without-f/--force) when discarding would lose locally-modified or staged content, or overwrite such a colliding untracked path; with-fit is overwritten. git'sreset --harddiscards silently. The guard re-checks each dropped path directly, so it also covers a tracked file that matches an ignore rule (.gitignore/.kitignore).<commit>defaults toHEAD.
Commands deliberately not implemented (by design)
Several Git commands that were once "wontfix-by-design" have been
reinstated under safety guards as part of the git-parity
porcelain-completion work: kit revert, kit mv, kit clean, and
kit reset --hard are all available and documented above. Each keeps a
kit data-loss guard — for example, clean and reset --hard refuse to delete or
discard without an explicit -f — rather than adopting git's silent
destructive default. Rename detection in diff/log output remains a
separate, unscheduled follow-up: kit mv stages the move
but status shows it as a delete plus add, not R.
Note: object garbage collection (kit gc) is shipped.
History-rewriting commands (commit --amend, reset, rebase) record
the superseded commit in the recovery log, and kit gc reclaims
unreachable objects once they fall outside the grace window. See
docs/specs/SPEC-GC.md.
Config keys
Stored in .kit/config as key = value lines. Kit refuses to load a config
file that has an unknown key, a line without =, or a malformed value. The
error names the file and the line.
Security-sensitive keys are user-scoped only. Kit warns and ignores them
if a repo's .kit/config sets them (a hostile repo must not be able to
redirect signing or trust).
Those keys — user.identity, signing_key, signer, key.*, attest.*,
ssh.*, trusted_remote_endpoint, admission_helper, grant.webauthn_rp, and
remote.<name>.admission_headers — live in the user config
($XDG_CONFIG_HOME/kit/config); set them with kit config <key> <value>,
which routes them to the user scope automatically.
| Key | Value | Default | Notes |
|---|---|---|---|
user.identity |
hex Identity | unset | User-scoped only; see below |
signing_key |
path | .kit/keys/default.key |
Ed25519 seed file; user-scoped only |
default_branch |
name | main |
Branch for kit init |
remote_endpoint |
URL / path | empty | Set via kit remote add |
remote_bucket |
name | empty | For s3 remotes |
remote_type |
file / http / s3 / ssh / memory |
auto | |
transport_auth |
bearer / envelope |
bearer |
Write-auth mode for kit+https:///kit+http://; envelope additionally Ed25519-signs writes with the commit-signing key (see signer/signing_key/key.ed25519_ref) |
http.sslCAInfo |
PEM certificate file path | unset | User-scoped only. Additional trust certificates for native Connect HTTPS remotes only. KIT_SSL_CA_FILE takes precedence. See HTTPS certificate trust. |
admission_helper |
absolute executable path | unset | User-scoped; invoked once for a 402 on a trusted remote. Its stdin includes untrusted server challenge content. The helper decides whether and how much to spend. |
remote.<name>.admission_headers |
comma-separated HTTP header names | empty | User-scoped additions to the admission helper's request allowlist; hard-reserved names are refused. |
ssh.strict_host_key_checking |
yes / no / accept-new |
inherit | User-scoped only |
ssh.user_known_hosts_file |
path | inherit | User-scoped only |
ssh.identity_file |
path | inherit | User-scoped only |
signer |
file / keystore |
file |
User-scoped commit signing source |
key.backend |
backend name | software |
User-scoped default for kit key |
key.default_ref |
<backend>:<label> |
software:default |
User-scoped fallback key ref |
key.ed25519_ref |
<backend>:<label> |
software:default |
User-scoped Ed25519 ref |
key.secp256k1_ref |
<backend>:<label> |
software:default-secp256k1 |
User-scoped secp256k1 ref |
key.p256_ref |
<backend>:<label> |
software:default-p256 |
User-scoped P-256 ref |
attest.signer |
repo-key / keystore / external |
repo-key |
User-scoped attestation signer |
grant.webauthn_rp |
<rp_id> <origin>... (repeatable; | separates entries in one value) |
unset | User-scoped only. Pins the WebAuthn relying parties kit grant/epoch/visibility accept for webauthn-p256 signatures; unset refuses them |
Keystore backend names include software, software-raw, macos-keychain,
linux-secret-service, systemd-creds, and yubikey
when the target build enables the corresponding backend feature. Security-
sensitive selector keys are ignored from repo-local config; set them in
$XDG_CONFIG_HOME/kit/config or with explicit command flags.
HTTPS certificate trust
For a private or local kit remote over Connect HTTPS (kit+https://),
supply a PEM file containing the CA certificates you trust:
kit config --global http.sslCAInfo /absolute/path/to/local-ca.pem
# Override the configured file for one command:
KIT_SSL_CA_FILE=/absolute/path/to/other-ca.pem kit fetch origin
http.sslCAInfo is user-scoped only. A repository file and a -c override
cannot set it, because a repository must not add trust roots. KIT_SSL_CA_FILE
overrides the configured value.
Configured relative paths start at the repository root; ~/ expands against
HOME. An environment file path is used as supplied.
The selected file adds certificates to the bundled Mozilla roots; certificate-chain
and hostname verification remain enabled. A missing, unreadable, empty,
or malformed selected file is a hard error. Keep private keys out of the
CA file; it contains certificates only.
These settings apply only to native kit remotes over the Connect transport:
HTTPS RPCs, pack uploads and downloads. The self-updater uses the OS trust
store and the release checksum when present; it does not read
KIT_SSL_CA_FILE / http.sslCAInfo. S3 remotes do not yet honor either
setting; private-CA S3 endpoints are not supported. Wasm/browser
certificate trust is unchanged.
The key shares its spelling with Git's CA-file setting;
kit uses its own KIT_SSL_CA_FILE environment variable.
Admission helper
The admission helper's stdin carries raw headers["www-authenticate"]
field values. One value can contain several challenges when the server's
platform combines repeated fields. Helpers MUST parse each value as an
RFC 9110 §11.6.1 challenge list, preserve challenge order, and distinguish
list separators from commas within auth-params and quoted strings.
The separate typed challenges array remains available. kit forwards
the raw fields without splitting them.
user.identity
The commit author Identity, encoded as [kind:u8][len:u16 LE][bytes]
in lowercase hex per docs/specs/SPEC-OBJECTS.md §9. Accepted shorthands at
parse time:
user.identity = ed25519:<64-char-hex> # kind=0x01, 32-byte pubkey
user.identity = mid:<u64 decimal> # kind=0x03, 8-byte LE opaque
user.identity = <raw-hex> # already-encoded Identity
When unset, the CLI derives an Ed25519 Identity from the signing key's public key at commit time. Most users don't need to set this.
ssh.*
Thin overrides for the ssh child process spawned by the SSH
transport. Empty string ("") means "do not pass the flag; inherit the
user's ~/.ssh/config default". See docs/SSH-SECURITY.md for the
recommended trust model.
URL schemes
kit remote add <url> accepts the strict kit+<scheme>://... form
only. Anything else is hard-rejected:
error: invalid remote URL '<input>': must start with 'kit+<scheme>://'
hint: URL must start with kit+<scheme>:// (e.g. kit+https://, kit+ssh://, kit+file://, kit+s3://)
Accepted schemes:
| Scheme | Form | Use |
|---|---|---|
kit+file |
kit+file:///abs/path |
local filesystem mirror |
kit+https |
kit+https://host[:port][/path] |
ConnectRPC gateway (for example, VCS Worker) |
kit+s3 |
kit+s3://endpoint/bucket[/prefix] |
S3-compatible object store |
kit+ssh |
kit+ssh://user@host[:port]/path |
SSH with the kit shell |
kit+memory |
kit+memory:// |
in-memory (testing only) |
kit+https (and loopback-only kit+http) speaks
kit.transport.v1.TransportService over ConnectRPC — see
docs/specs/SPEC-TRANSPORT-CONNECT.md. Any /path on the URL is
accepted (for shape-consistency with the other schemes) but currently
has no wire effect: every RPC resolves to the fixed
/kit.transport.v1.TransportService/<Method> path regardless.
Write RPCs against a server that requires the Ed25519 write envelope
need
kit config transport_auth envelope — see the config table above and
SPEC-TRANSPORT-CONNECT §7.3.
The URL path names the repository as <namespace>/<name>
(SPEC-TRANSPORT-CONNECT §7.4). Two namespace grammars are accepted: the
self-certifying ed25519-<64 lowercase hex> and 0x<40 lowercase hex>
forms, and the opaque authority-mode form [a-z0-9][a-z0-9._-]{0,71}
other than root, ed25519-… and 0x…
(SPEC-TRANSPORT-CONNECT §7.4). An
authority-mode deployment can therefore use a UUIDv7, for example
kit+https://vcs.example/019c88c3-a904-7bd1-8a5d-3182a0c6978a/repo. The
URL does not say which mode the server runs; the server still enforces
only its own grammar and answers invalid_argument for the other. Plain
https:// is not a sync remote — at a server's origin it serves
objects by repository path (SPEC-HTTP-OBJECTS) — so kit clone https://… is refused with a hint to add the kit+ prefix.
Private reads on an authority-mode server are decided by the
deployment's authority hook on the signer's key, not by grants or owner
statements. Read one by signing each request: run kit config transport_auth envelope and kit config trusted_remote_endpoint <url>,
with a user-scoped signing_key as for any private clone. To a caller
the hook does not admit, a private repository answers not_found,
indistinguishable from a missing one. KIT_API_TOKEN is still sent as
Authorization: Bearer …, but the server does not pass it to hooks: it
discards an Authorization value unless it is a single admission
credential, and an unsigned request captures no credentials at all.
See docs/specs/SPEC-TRANSPORT.md for the other transports' wire
protocols.
Version output contract
kit version prints exactly:
kit <X.Y.Z>\n
Downstream packagers (Homebrew) assert on this substring. The format is pinned by a snapshot test in the CLI crate.
*nix conventions
kit follows common POSIX CLI conventions so shell scripts, pipelines, and interactive use behave predictably.
Exit codes
Based on BSD sysexits(3):
| Code | Constant | Meaning |
|---|---|---|
| 0 | ok |
Success |
| 1 | general_error |
Catch-all for other failures |
| 64 | usage |
Wrong args or unknown subcommand |
| 65 | dataerr |
Malformed input (corrupt object, bad hash) |
| 66 | noinput |
Missing / unreadable input file |
| 69 | unavailable |
Transport could not connect |
| 70 | software |
Internal consistency error (a bug, not bad input) |
| 73 | cantcreat |
Cannot create output file |
| 75 | tempfail |
Temporary failure; retry is safe |
| 76 | protocol_error |
Bad URL scheme or malformed server response |
| 77 | noperm |
Permission denied |
| 78 | config_error |
Unknown config key or invalid value |
The constants live in rust/crates/kit-bin/src/exit.rs. Shell scripts
can distinguish user typos (64) from transient failures (75) without
parsing stderr.
Environment variables
GIT_EDITOR/EDITOR/VISUAL— the editorkit commitopens when neither-mnor-Fis supplied, tried in that order. If none is set it falls back tovi, matching git. In a headless or non-interactive context (CI, an agent), always pass-m/-Fso the commit never blocks on an editor.NO_COLOR— if set (any value, including empty) ANSI color on stdout is suppressed. See https://no-color.org.CLICOLOR_FORCE=1— force ANSI color even when stdout is piped.NO_COLORoverrides this.SSH_AUTH_SOCK— standard OpenSSH agent socket, used bykit+ssh://transports.KIT_SSL_CA_FILE— PEM certificate file for additional native Connect HTTPS remote trust only, overridinghttp.sslCAInfo. The self-updater is excluded by design; S3 remotes are not yet covered. Certificates are added to the Mozilla roots, with chain and hostname verification enabled. Missing, unreadable, empty, or malformed files are hard errors. See HTTPS certificate trust.XDG_CONFIG_HOME,XDG_DATA_HOME,XDG_CACHE_HOME,XDG_STATE_HOME— XDG Base Directory roots for user-level config, keystore, cache, and state respectively. Defaults per the freedesktop spec (~/.config,~/.local/share,~/.cache,~/.local/state).
File layout
| Path | Purpose |
|---|---|
.kit/ |
Repo-local state (like .git/) |
.kit/config |
Repo-local config — overrides user-level values |
.kit/keys/default.key |
Repo-local Ed25519 signing key |
.kit/index |
Staging index |
.kit/index.lock |
Held by commit/checkout/merge/rebase |
.kit/COMMIT_EDITMSG |
Scratch file for kit commit without -m |
$XDG_CONFIG_HOME/kit/config |
User-level config (cross-repo defaults) |
$XDG_CONFIG_HOME/kit/grants/ |
Your grant store: <grant id>.grant files (0700/0600), never repo-scoped |
$XDG_DATA_HOME/kit/keys/ |
User-level keystore (optional) |
$XDG_CACHE_HOME/kit/ |
User-level cache |
$XDG_STATE_HOME/kit/ |
User-level state |
Repo-local values in .kit/config win over user-level config except for
security-sensitive signer, keystore, external-signer, and identity selector keys
that kit intentionally accepts only from user scope or explicit flags.
Signals
SIGINT(Ctrl-C) andSIGTERMset a graceful-shutdown flag. Long-running operations (push, pull, clone, log) poll it at natural checkpoints and exit withtempfail(75) so the operation can be retried. Wired viasignal-hook'sflagmodule so the CLI stays under its crate-level#![deny(unsafe_code)].SIGPIPEis ignored. Pipelines likekit log | head -1exit cleanly without a "Broken pipe" message — the next write just propagatesEPIPEas a normal I/O error. This is the Rust runtime default (since 1.65); kit does not register over it. The behavior is pinned by an integration test (tests/sigpipe.rs).
Man page
# System install (requires root):
sudo cp man/kit.1 /usr/local/share/man/man1/
# User install (preferred if ~/.local/share/man is on MANPATH):
mkdir -p ~/.local/share/man/man1
cp man/kit.1 ~/.local/share/man/man1/
man kit
Homebrew users installing from the tap get the man page automatically.
Shell completions
# bash (macOS / Homebrew)
cp completions/kit.bash /usr/local/etc/bash_completion.d/
# bash (most Linux)
sudo cp completions/kit.bash /etc/bash_completion.d/
# zsh (anywhere on $fpath; example for Homebrew)
cp completions/kit.zsh /usr/local/share/zsh/site-functions/_kit
Then restart your shell (or run compinit on zsh). Completion covers
the full subcommand list plus common flags; per-argument completion
(branch names, remote URLs) is deferred to a future release.