Short user-facing reference for the mkit 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).
mkit init # create .mkit/ in the current directory
mkit keygen # generate an Ed25519 signing key
echo hello > README.md
mkit add README.md
mkit commit -m "first commit"
mkit logCommits 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.
These are parsed before the subcommand (so they apply to repo discovery too), like git:
mkit -C <path> <command>— run as ifmkitwere started in<path>. Repeatable; relative paths resolve against the prior-C(git semantics).mkit -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, and dangerouscore.*keys are dropped. So-ccannot spoof the signed author or redirect trust; use it for inert/allowlisted keys (for example,-c user.email=ci@example.com).mkit --no-pager/-P/--paginate— accepted no-ops. mkit never paginates (better for agents and scripts); the flags exist so defensive invocations likemkit --no-pager logdon't error.
--git-dir, --work-tree, and --exec-path are non-goals — mkit uses
the .mkit/ marker and has no split work-tree / exec-path model.
Working-tree commands:
mkit init— create a new repository in.mkit/.mkit add [-A|-u] [-f] [-p] <path>.../mkit 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/.mkitignore) 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.mkit 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 mkit 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 #176 restore guards); use--cachedto keep the file or--forceto discard the local changes.
mkit 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,mkit status/mkit 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) — an mkit 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 an mkit 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.
mkit 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 mkit'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.mkit 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 changemkit 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 an mkit 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. (Color forstatus/log/branchis a tracked follow-up.)-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).mkit worktree add <path> [<commit-ish>]/list [--porcelain]/remove [--force] <path>/prune [--dry-run]— manage linked working trees (git-worktree parity, #493). 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; mkit'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.mkitis a pointer file (mkitdir: <path>) into the main repository's.mkit/worktrees/<id>/. Interim limit:mkit gcrefuses to run while linked worktrees exist (cross-tree root collection is a tracked follow-up phase of #493) — remove or prune them first.mkit 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 (mkit 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":"…"}.mkit sparse-checkout— manage sparse checkout patterns.
History / commits:
mkit commit [-a|--all] [--amend] [--author <spec>] [-m <msg> | -F <file>] [--format=json]— create a signed commit from the staging index. The index is built bymkit add/mkit rm;commitwith an empty index is an error. Usemkit 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.mkit 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 move is recorded in the ref-history journal. 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 bymkit gconce it falls out of the retention window. Aftermkit 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.-S/--gpg-sign[=<keyid>]and--no-verifyare accepted no-ops (mkit always signs and has no hooks);--no-editmatches mkit's default amend behavior.--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>}.mkit log [--oneline] [--abbrev-commit] [--abbrev[=N]] [--format=json] [--graph] [--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.--graphis accepted for compatibility but is currently a no-op (no ASCII graph is drawn); see "Divergences from Git" below.--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, mkit 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.mkit reflog [<ref>] [--format=json] [-n N]— read-only view of a branch's recorded movement history. Defaults to the branchHEADpoints at (a detachedHEADneeds an explicit<ref>, since the ref-history journal is keyed per-branch). Walks the branch tip's first-parent chain newest-first, addressing each entry<ref>@{N}(@{0}is the current tip). The underlying journal (the per-branch commit-history MMR written on every branch advance) stores a count of advances and a tamper-evident root, but its leaf digests cannot be decoded back to commit hashes — so the listed hashes are reconstructed from the reachable chain. On a build with--features history-mmreach entry is additionally cross-checked against the journaled MMR root via an inclusion proof, and the recorded-advance count is printed.--format=jsonemits JSONL with keysref,selector,index,hash,title,journaled(true/false/null). This is not a full Git reflog:@{N}indexes the reachable first-parent chain, so commits superseded bycommit --amendor a reset are not listed; see "Divergences from Git" below.mkit 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. mkit's documented divergences (consistent with--format=jsonand thelogprecedent): object ids are 64-hex;author/committercarry mkit's Identity string, notName <email>(author-mail/committer-mailare empty and both*-tzare+0000, since mkit 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 for git-surface compatibility, but because mkit'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. (mkit does not auto-read a.git-blame-ignore-revsfile or ablame.ignoreRevsFileconfig key; pass the file explicitly.)--ignore-rev-precise(mkit-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); mkit 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 (mkit 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^; mkit'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.mkit 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 registrymkit trustmanages, failing closed (nonzero exit, distinct from a bad-signature failure) on an unlisted signer.--trust-rootsdefaults to$XDG_CONFIG_HOME/mkit/trust-roots.toml; an in-repo path is refused unless passed explicitly (seemkit trustbelow anddocs/THREAT-MODEL.md§5).mkit cat <hash>— display an object by its hash.mkit hash <file>— hash a file and store it as a blob.mkit tree— snapshot the working directory as a tree object.
Read-only plumbing (object/ref inspection, for scripts and agents):
mkit cat-file (-t | -s | -p) <object>/mkit cat-file --batch— inspect a stored object (likegit cat-file).-tprints the type (blob/tree/commit/tag; mkit'sremixis the one non-git type);-sprints the size (the content byte length for blobs — matching git — but mkit'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 mkit-shaped, as with-p.<object>resolves through the shared revspec grammar (hash, ref,HEAD,HEAD~n/^).mkit show [--stat] [<object>...]— display objects (likegit show), defaulting toHEAD. A commit/remix prints a header (commit <hash>,Author:,Date:, message indented four spaces — matchingmkit log) followed by the unified diff against its first parent; that diff body is produced by the same code asmkit 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 mkit'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.mkit 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.mkit 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— mkit has no merge stages).--otherslists untracked worktree files instead;--exclude-standarddrops ignored ones (per.gitignore/.mkitignore) 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).mkit rev-parse [--verify] [--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;--verifyis accepted for git-script compatibility but is a no-op — mkit always errors on an unresolvable revision, with or without it;--show-toplevelprints the repository root (the directory holding.mkit, found by walking up from cwd).mkit 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.)mkit 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.mkit 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.mkit 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.mkit 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).mkit 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 — an mkit safety divergence (git's plumbing would, leaving HEAD dangling).mkit 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 (#652): scripts should use it instead ofls/caton.mkit/refs/, which is not a supported interface.mkit ref cat <name>— print the resolved hash for exactly one ref, followingHEAD's symbolic indirection (HEADis mkit'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:
-
mkit 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.mkit/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:
mkit attest --algorithm ed25519 \ --additional-signer "algorithm=p256,signer=repo-key" \ --predicate-type https://example.com/review/v1 \ --predicate-file review.json -
mkit 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/mkit/trust-roots.toml. mkit 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:
mkit verify-attest --trust-roots ~/.config/mkit/trust-roots.toml -
mkit trust add <keyid> <pubkey-hex> [--kind <kind>] [--trust-roots <path>] [--force]/mkit trust list [--trust-roots <path>] [--json]/mkit trust remove <keyid> [--trust-roots <path>] --yes— manage the trust-roots registrymkit verify --trustedandmkit 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-sec1,secp256k1, andbls12381-thr(with thebls-thresholdfeature) only 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:
mkit keygen --print-pubkey # ed25519:<hex> mkit trust add ed25519:<hex> <hex> # trust your own key mkit trust list mkit verify --trusted HEAD
Branches / refs:
mkit branch/mkit branch <name>/mkit 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.mkit branch -D <name>— force-delete. mkit 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).mkit 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.mkit branch --show-current— print the checked-out branch name (empty output on a detached HEAD), likegit branch --show-current.mkit 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 — somkit 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,mkit branch --list "release/*", ormkit branch --list mainto test existence).mkit 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).mkit 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>'.mkit switch <branch>/mkit 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: mkit does not yet track a previous branch.)mkit 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.mkit/.git) is left untouched (git needs the double-force-ffto remove one, which mkit 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).mkit tag [--format=json]— list, create, or delete tags.mkit tag(no args) — list tags; annotated/signed tags are marked.mkit tag -l [<pattern>]— list tags, optionally filtered by a shell glob (for example,mkit tag -l 'v*'), reusingbranch'swildmatchengine.mkit tag <name> [<commit>]— create a lightweight tag (a ref pointing straight at<commit>, default HEAD).mkit tag -a <name> [-m <msg>] [<commit>]— create an annotated tag object (target, tagger identity, message, timestamp). Without-m,$EDITORis launched.mkit tag -s <name> [-m <msg>] [<commit>]— create a signed annotated tag: an Ed25519 signature over the canonical tag bytes under the distinctmkit.tagdomain. Verify withmkit verify <name>.mkit 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.
mkit 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 withmkit commit(which records a two-parent merge commit) ormkit 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).mkit 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 (runmkit commitwhen ready; the result has the current branch as its single parent). A-npick that conflicts is refused before anything is written — mkit cannot represent a staged-but-unresolved conflict, and a latermkit 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 (mkit 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).mkit 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.mkit 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.mkit 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 asMKIT_BISECT_COMMIT. (mkit'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).mkit 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 mkit 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.
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
.mkit/(see below) and exits non-zero.
To finish, for each conflicting path: edit the worktree file to its
resolved content (remove all conflict markers), mkit add <path>, then:
mkit 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 mkit rm, and a file replaced
by a symlink or directory without mkit 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:
mkit merge --abort # restore HEAD, branch ref, index, and worktree
mkit 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.
All live under .mkit/; rebase keeps its state inside
.mkit/rebase-apply/. These use Git-compatible names plus one documented
mkit sidecar. The .mkit/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 |
mkit-conflicts |
mkit 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 mkit-conflicts sidecar when paused. actions is parallel to todo (pick/reword/squash/fixup, from rebase -i); absent ⇒ all pick |
Remote / sync:
-
mkit 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. -
mkit 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 withmkit+<scheme>://(see below). -
mkit remote set [<name>] <url>— alias formkit remote add. -
mkit remote remove <name>(aliasrm) — delete a named remote. The reserved namedefaultclears the flatremote_endpoint. -
mkit 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 #97 credential-trust gate is preserved). -
mkit remote get-url <name>— print a remote's URL (usedefaultfor the flat default remote).mkit remote set-url <name> <url>— change an existing remote's URL (validated likeremote add). -
mkit 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: mkit'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.mkit/configinstead of the implicit flatdefaultremote (mirrorsmkit 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.
-
mkit 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. -
mkit 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 (issue #692, default ON).
clone/pull/fetchverify every commit/remix/tag the fetch newly introduces — the same checkmkit verify <rev>runs manually (mkit_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.mkit/config(see THREAT-MODEL.md §4).--allapplies the same verification (and the same opt-out) to every remote it iterates. -
mkit 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>}on success, or{"ok":false,"rejected":true, "branch":"...","error":"..."}on a non-fast-forward (CAS) rejection ({"ok":false,"error":"..."}for any other failure). -
Transfer progress (#711).
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); mkit never fabricates git'sEnumerating/Counting/Compressing objectsorTotal N (delta D)lines — mkit'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, andMKIT_PROGRESS=always/neveroverrides the tty-detection explicitly (mirrorsNO_COLOR/CLICOLOR_FORCE). -
mkit serve <path>— internal SSH transport server. Speaks the mkit-rpc SSH framing on stdin/stdout by default. -
mkit serve <path> --listen-enc <addr>— bind a TCP socket on<addr>(for example,0.0.0.0:9418) and serve the same protocol over an encrypted-stream transport. Requires building the binary with--features enc-transport. Fail-closed: the listener refuses to bind unless one of the following is supplied:--enc-authorized-peers <PATH>— an allowlist of authorized client public keys, one per line (64-hex or 43-char url-safe base64;#comments and blank lines ignored). A client whose static ed25519 key is not listed is rejected at the handshake and receives no data. This path MUST be CLI-supplied or user-scoped — peer-authorization is never read from repo-local.mkit/config.--unsafe-allow-any-enc-peer— a development escape that accepts ANY peer. Prints a loud warning; never use in production. These two flags are mutually exclusive.
Post-handshake resource bounds (slow-loris hardening):
--enc-idle-timeout-secs <SECS>— per-frame idle timeout applied after the handshake completes. A peer that does not send its next verb/upload frame within this window has its session dropped, so a peer that finishes the handshake then stalls cannot pin a worker plus socket indefinitely.0disables the timeout (not recommended). Default:60.--enc-handshake-timeout-secs <SECS>— overall deadline for completing the cryptographic handshake. SPEC-TRANSPORT-ENC §6.2 recommends tightening to ≤5–10s on real networks; the default is deliberately generous. Default:60.
--enc-server-key <PATH>selects the server's stable raw 32-byte ed25519 key file (auto-created with0600/0700hardening on first run). When allowlisting and the flag is omitted, the key is auto-created at the user-scoped default~/.config/mkit/enc/server.keyso the advertised?pubkey=is stable across restarts. Only the unsafe allow-any mode without a key file falls back to an ephemeral per-process key. The server prints its public key to stderr at startup; clients dialmkit+enc://<host>:<port>?pubkey=<key>after copying that key out-of-band. A client may pin its own identity (so an allowlisting server can recognize it across restarts) by pointing theMKIT_ENC_CLIENT_KEYenvironment variable at a user-scoped raw 32-byte key file; otherwise the client uses an ephemeral key. The default port advertised bymkit+enc://URLs when none is supplied is 9418. Full keystore integration is deferred (see SPEC-TRANSPORT-ENC §6.2). -
mkit serve <path> --http <addr>— self-hosted Connect remote: bind<addr>(for example,0.0.0.0:8443) and hostmkit.transport.v1.TransportService(SPEC-TRANSPORT-CONNECT) over axum/HTTP, instead of the SSH-frame protocol. Requires building with--features http-transport. This is a plaintext HTTP listener — put a TLS-terminating reverse proxy in front for production use (or bind to loopback and tunnel). Fail-closed, mirroring--listen-enc: refuses to bind unless one of the following is supplied:--http-token <TOKEN>(or theMKIT_API_TOKENenvironment variable — the same variablemkit+https://clients already send asAuthorization: Bearer <token>, SPEC-TRANSPORT §5.2) — every RPC (unary and streaming, includingUploadPack/DownloadPack) is rejected withunauthenticatedunless it carries a matching bearer token, checked in constant time.--unsafe-allow-any-http-peer— a development escape that accepts ANY caller with no authentication at all. Prints a loud warning; never use in production. These two are mutually exclusive, and mutually exclusive with--listen-enc.Ctrl-C/SIGTERMdrain in-flight requests before exiting (the same cooperative shutdown flag the rest of the CLI uses).
-
mkit pack-shard <hash> [--out <dir>] [--force]— encode a stored pack into Reed-Solomon shards plus a manifest, ready to publish to an HTTP / S3 origin. Producer side of the SPEC-PACK-SHARDS wire-format and transport-delivery stage. The pack must already be in the local object store. Output layout:<out>/packs/<hex>/shards/<index>and<out>/packs/<hex>/shards.manifest. The manifest is written last so racing readers either see "no manifest" (clean fall-through to the monolithic pack) or "manifest plus all shards". Requires building the binary with--features pack-shards. -
mkit 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 mkit-only fields (signer, signature, identity, annotation slots) ride inmkit-*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.mkit/git/<name>/; mkit-side history rewrites mirror as force-pushes. Unless--no-attest, each exported head gets a signedgit-bridge/v1DSSE attestation (saved likemkit attest, published on the mirror'srefs/mkit/attestationsref — fetch it with an explicit refspec; git clones skip it by default). Attestation signing uses the same signer resolution asmkit 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. -
mkit 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 mkit 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 (.mkit/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.mkit/git/<name>/with agit-import/v1attestation per head. Unrepresentable refs skip per-ref with warnings (submodules, mkit-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). -
mkit 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.mkit git pull [--remote-name <name>] [--key <path>] [--json]additionally fast-forwards the current branch; divergence refuses with the executable hint (mkit merge <name>/<branch>— imported history is ordinary mkit history, integrated natively). -
mkit 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. -
mkit 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 mkit 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). -
mkit git status— one block per.mkit/git/<name>/state dir: direction, recorded source/dest, pinned importer key, tracking refs (import) and lease positions (export), staging health. -
mkit 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,mkit git format-patch upstream/main -o patches/.All
mkit gitsubcommands require building the binary with--features git-bridge(alias:git-export); they shell out togit.
Agent integration:
-
mkit mcp [--repository <path>]— start a Model Context Protocol server on stdio so LLM agents (Claude, Cursor, etc.) can drive local mkit 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— mkit'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.mkit/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. Register with an MCP client, for example:claude mcp add mkit-repo -- mkit mcp --repository /path/to/repo
Config / keys / version:
mkit keygen— generate a new Ed25519 signing keypair.mkit key generate [--backend B] [--label L] [--algorithm ALG]— generate a signing key in a keystore backend.ALGised25519,secp256k1, orp256; default ised25519.mkit key list [--backend B] [--json]— list keys visible to a backend.mkit key import --algorithm ALG (--hex HEX | --file PATH) [--backend B] [--label L]— import 32 bytes of signing key material where the backend allows import.mkit key export [--backend B] [--label L] [--algorithm ALG] --unsafe-print-secret— export extractable key material. Non-extractable backends fail closed.mkit key delete [--backend B] [--label L] [--algorithm ALG] --yes— delete exactly one backend key where deletion is supported.mkit config [--format=json]— show all configuration values.--format=jsonemits a flat JSON object with every known key.mkit config <key> [--format=json]— show one value.mkit config --unset <key>— remove a configuration value. Deletes from whichever scope asetof that key would use — the repo layer (<repo>/.mkit/config) for a repo-safe key, or the user-scoped layer ($XDG_CONFIG_HOME/mkit/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.mkit 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(mkit 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.mkit 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.user.name/user.email— git-compatibility aliases. They are accepted and round-trip likegit config user.name, but are non-authoritative: mkit's commit author is cryptographic (user.identityor the signing key's derived Identity), and these values never feed it. They are repo-safe (stored in.mkit/config) precisely because they cannot influence the signed author — unlikeuser.identity, which stays user-scoped and inREPO_FORBIDDEN_KEYSso a hostile clone cannot spoof authorship.core.*— git-compatibility keys. An inert allowlist (core.autocrlf,core.bare,core.filemode,core.ignorecase,core.quotepath,core.symlinks) is accepted and round-tripped for parity but not honored — mkit has no CRLF translation, tracks exec bits natively, etc. Dangerouscore.*keys that would change what mkit runs (core.sshCommand,core.pager,core.editor,core.hooksPath,core.fsmonitor) are rejected rather than stored. Names match case-insensitively and canonicalize to lowercase, like git.
mkit self update [--version <tag>] [--check] [--allow-downgrade] [--format human|json]— update this binary in place from a signed GitHub Release. The downloaded archive is checked against its sha256 sidecar asset when the release publishes one; this runs entirely in-process, nocosignand no GitHub attestation API are involved. Only installer-managed binaries are updated: the.mkit-installed-tagreceipt written byinstall.shmust sit next to the (canonicalized) executable, and the global$MKIT_STATE_DIR/installed-tag(default~/.local/state/mkit) must agree with it when present. Homebrew/cargo installs are refused with channel-specific guidance (brew upgrade mkit,cargo install --locked mkit-cli). 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 exactlymkit <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),MKIT_STATE_DIR,MKIT_SELF_UPDATE_API_BASE(test/mirror override). - There is no background update check — the command only ever acts when invoked. Not yet supported on Windows.
mkit version— print the version. Emits exactlymkit <X.Y.Z>\n. The top-levelmkit --version/mkit -Vflags are aliases of this subcommand and emit the identical string. Note this ismkit <X.Y.Z>, not Git'sgit version <X.Y.Z>form — an intentional, documented divergence (the string is pinned by packaging asserts).
mkit reads ignore patterns from both .gitignore and .mkitignore at
the repository root. .gitignore is applied first and .mkitignore last, so
under gitignore's last-match-wins rule a repo's own .mkitignore 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. .mkitand.gitare always ignored (matched on the basename, ASCII case-insensitively).
Deferred (documented non-goals for now, #256): nested per-directory
ignore files (only the repo root is read), global excludes
(core.excludesFile), and escaped trailing spaces.
mkit's local commands intentionally diverge from Git in a few places. These are documented behaviors, not bugs, with tracked follow-ups:
- Human output goes to stderr; stdout is reserved for porcelain/data.
mkit's command summaries (
commit/merge/push/status…) are shaped like git's but are written to stderr, somkit 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; mkit 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: mkit'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 (#711) — real counts only; see "Transfer progress" above. mkit remotelists remote names only (git-shaped); usemkit remote -vfor<name>\t<url> (fetch)/(push).mkit log --graphis a no-op (v1 non-goal). The flag is accepted for compatibility so existing scripts don't break, but no ASCII commit graph is drawn. Full graph parity is unachievable because mkit's defaultlogbody already diverges from git'scommit/Author/Date(mkitIdentityplus 64-hex ids); an--oneline --graphrenderer for the linear/DAG case is a tracked post-v1 follow-up, not a v1 blocker.mkit log --author/--grepare plain substring matches, not regexes. git's--author/--grepdefault to POSIX extended-regex matching against a free-textName <email>header; mkit 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.mkit 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.mkit 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.mkit 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.mkit 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; mkit 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).mkit 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/.mkitignore).<commit>defaults toHEAD.
Several Git commands that were once "wontfix-by-design" have been
reinstated under safety guards as part of the git-parity
porcelain-completion work (#250): mkit revert, mkit mv, mkit clean, and
mkit reset --hard are all available and documented above. Each keeps an
mkit 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 (decision #226): mkit mv stages the move
but status shows it as a delete plus add, not R.
Note: object garbage collection (mkit gc) is shipped (issue #233).
History-rewriting commands (commit --amend, reset, rebase) record
the superseded commit in the recovery log, and mkit gc reclaims
unreachable objects once they fall outside the grace window. See
docs/specs/SPEC-GC.md.
Stored in .mkit/config as key = value lines — except security-sensitive
keys, which are user-scoped only and ignored if set in a repo's
.mkit/config (a hostile repo must not be able to redirect signing or trust).
Those keys — user.identity, signing_key, signer, key.*, attest.*,
ssh.*, and trusted_remote_endpoint — live in the user config
($XDG_CONFIG_HOME/mkit/config); set them with mkit 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 | .mkit/keys/default.key |
Ed25519 seed file; user-scoped only |
default_branch |
name | main |
Branch for mkit init |
remote_endpoint |
URL / path | empty | Set via mkit 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 mkit+https:///mkit+http://; envelope additionally Ed25519-signs writes with the commit-signing key (see signer/signing_key/key.ed25519_ref) |
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 |
legacy / keystore |
legacy |
User-scoped commit signing source |
key.backend |
backend name | software |
User-scoped default for mkit 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 |
Keystore backend names include software, software-raw, macos-keychain,
windows-credential, 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/mkit/config or with explicit command flags.
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.
The legacy author_mid = <N> key from pre-0.1.0 is rejected at
mkit config time with a hint pointing to user.identity. parseConfig
silently ignores stray author_mid lines that may still be on disk.
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.
mkit remote add <url> accepts the strict mkit+<scheme>://... form
only. Anything else is hard-rejected:
error: invalid remote URL '<input>': must start with 'mkit+<scheme>://'
hint: URL must start with mkit+<scheme>:// (e.g. mkit+https://, mkit+ssh://, mkit+file://, mkit+s3://)
Accepted schemes:
| Scheme | Form | Use |
|---|---|---|
mkit+file |
mkit+file:///abs/path |
local filesystem mirror |
mkit+https |
mkit+https://host[:port][/path] |
ConnectRPC gateway (for example, VCS Worker) |
mkit+s3 |
mkit+s3://endpoint/bucket[/prefix] |
S3-compatible object store |
mkit+ssh |
mkit+ssh://user@host[:port]:path |
SSH with the mkit shell |
mkit+memory |
mkit+memory:// |
in-memory (testing only) |
mkit+https (and loopback-only mkit+http) speaks
mkit.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
/mkit.transport.v1.TransportService/<Method> path regardless.
Write RPCs against a deployment that requires the Ed25519 write envelope
(for example, apps/vcs-worker, the reference server) need
mkit config transport_auth envelope — see the config table above and
SPEC-TRANSPORT-CONNECT §7.3.
See docs/specs/SPEC-TRANSPORT.md for the other transports' wire
protocols.
mkit version prints exactly:
mkit <X.Y.Z>\n
Downstream packagers (Homebrew, Scoop) assert on this substring. The format is pinned by a snapshot test in the CLI crate.
mkit follows common POSIX CLI conventions so shell scripts, pipelines, and interactive use behave predictably.
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 |
| 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/mkit-cli/src/exit.rs. Shell scripts
can distinguish user typos (64) from transient failures (75) without
parsing stderr.
GIT_EDITOR/EDITOR/VISUAL— the editormkit commitopens when neither-mnor-Fis supplied, tried in that order. If none is set it falls back tovi(Unix) /notepad(Windows), 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 bymkit+ssh://transports.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).
| Path | Purpose |
|---|---|
.mkit/ |
Repo-local state (like .git/) |
.mkit/config |
Repo-local config — overrides user-level values |
.mkit/keys/default.key |
Repo-local Ed25519 signing key |
.mkit/index |
Staging index |
.mkit/index.lock |
Held by commit/checkout/merge/rebase |
.mkit/COMMIT_EDITMSG |
Scratch file for mkit commit without -m |
$XDG_CONFIG_HOME/mkit/config |
User-level config (cross-repo defaults) |
$XDG_DATA_HOME/mkit/keys/ |
User-level keystore (optional) |
$XDG_CACHE_HOME/mkit/ |
User-level cache |
$XDG_STATE_HOME/mkit/ |
User-level state |
Repo-local values in .mkit/config win over user-level config except for
security-sensitive signer, keystore, external-signer, and identity selector keys
that mkit intentionally accepts only from user scope or explicit flags.
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 likemkit 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); mkit does not register over it. The behavior is pinned by an integration test (tests/sigpipe.rs).
# System install (requires root):
sudo cp man/mkit.1 /usr/local/share/man/man1/
# User install (preferred if ~/.local/share/man is on MANPATH):
mkdir -p ~/.local/share/man/man1
cp man/mkit.1 ~/.local/share/man/man1/
man mkitHomebrew users installing from the tap get the man page automatically.
# bash (macOS / Homebrew)
cp completions/mkit.bash /usr/local/etc/bash_completion.d/
# bash (most Linux)
sudo cp completions/mkit.bash /etc/bash_completion.d/
# zsh (anywhere on $fpath; example for Homebrew)
cp completions/mkit.zsh /usr/local/share/zsh/site-functions/_mkitThen 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.
mkit exposes a git-compatible CLI surface, so existing git habits — and AI
agents that emit git <cmd> — can drive it. contrib/git-shim/mkit-git is an
opt-in forwarder (git <args> → mkit <args>) that you install yourself;
it is never installed automatically and never shadows the real git.
# simplest (interactive shells):
alias git=mkit
# or, to also cover scripts/agents, put the shim on PATH as `git` for a
# specific project/environment, ahead of the real git:
ln -s "$PWD/contrib/git-shim/mkit-git" ~/.local/mkit-shim/git
export PATH="$HOME/.local/mkit-shim:$PATH"
command -v git && git version # verify it resolves to the shimSee contrib/git-shim/README.md for details. The shim is a pure forwarder:
an unsupported command exits with mkit's usage error rather than falling
through to real git.