CLI.md

This Kit feature is not available yet.

In docs/

Preview

Text · 121.1 KiB

mkit — CLI reference

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).

Quick start

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 log

Commits are signed with your Ed25519 key and stored locally. Your author Identity is automatically derived from your signing key's public key — no config needed.

Global flags

These are parsed before the subcommand (so they apply to repo discovery too), like git:

  • mkit -C <path> <command> — run as if mkit were 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 — the REPO_FORBIDDEN_KEYS set) are refused with a warning, and dangerous core.* keys are dropped. So -c cannot 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 like mkit --no-pager log don'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.

Subcommand reference

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/--all stages every change in the worktree including deletions of tracked files (takes no path arguments). -u/--update restages only already-tracked files — updating modified ones and recording deletions — without adding untracked paths (takes no path arguments). -A and -u are mutually exclusive. An explicitly-named path that is ignored (by .gitignore/.mkitignore) is refused unless -f/--force is given (git parity); already-tracked paths are never subject to ignore. -p/--patch interactively presents each hunk between the staged (or HEAD) blob and the worktree file and stages only the accepted ones — per hunk: y stage, n skip, a stage this and all later hunks, d skip this and all later hunks, q quit (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 plain add); a path that escapes the repo through a symlinked parent is refused. The hunk view and prompts go to stderr; s (split) and e (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, rm refuses to destroy a locally-modified tracked file (a dirty-worktree guard in the spirit of the #176 restore guards); use --cached to keep the file or --force to discard the local changes.
  • mkit mv [-f|--force] <source>... <dest> — move or rename a tracked path and stage the change. mv <src> <dst> renames src to dst, or moves it into dst when dst is 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 diff detect the move as an exact rename and report git's R by default; --no-renames shows the underlying deletion plus addition instead.
    • -f/--force — overwrite an existing destination. Without it, mv refuses to clobber an existing path (matching git's mv guard) — 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 newdir renames the directory, and mv dir existing-dir/ moves it into the target (becoming existing-dir/dir). The directory is renamed in a single filesystem operation, so untracked files inside it travel with it (exactly like git 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 with mv either), 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/--short is an alias for --porcelain=v1; both select the same renderer. Porcelain emits one entry per line in git status --porcelain=v1 format (XY <path>, with mkit's T for ModeChanged as the only non-git extension). --porcelain=v2 selects 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> always N...) and ? <path> for untracked — with no rename (2) records and no --branch header lines. Paths with special bytes are C-style quoted (matching git's default core.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 change mkit commit would 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 of a and b against b (git semantics). --merge-base is the flag spelling of that symmetric range: with two revisions it diffs merge-base(a, b) against b, and with one revision it diffs merge-base(<rev>, HEAD) against the worktree (like git diff --merge-base). Revisions may be refs, commit hashes, short hashes, or HEAD~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: a diff --git a/<path> b/<path> header per changed path (with new file mode/deleted file mode/old mode/new mode lines, an index <old>..<new> <mode> line, and --- a/<p>/+++ b/<p> using /dev/null for adds/deletes) followed by @@-delimited hunks (or Binary files … differ for non-text blobs). The hunk algorithm is the Myers diff with git's change-compaction, so the output byte-matches git diff apart from the abbreviated index ids, which are BLAKE3 prefixes rather than SHA-1 (git's optional indent heuristic is not applied). --name-only instead lists one changed path per line; --name-status prefixes each with a status letter (A/D/M; T marks an mkit mode change). --stat shows a diffstat: one <name> | <count> <graph> row per file (name column padded, +/- graph scaled to the terminal width like git diff --stat, honoring COLUMNS; default 80) followed by a N files changed, … insertions(+), … deletions(-) summary; binary files render Bin <old> -> <new> bytes. In --name-only/--name-status /--stat modes special-byte paths are C-style quoted (git core.quotePath); -z instead NUL-terminates records and emits raw paths (in --name-status -z the status letter and path are each their own NUL-terminated field). -z is only valid with --name-only / --name-status. --exit-code exits 1 when there are differences (0 when none), still printing the patch; --quiet is 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 default auto colorizes only on a tty (honoring NO_COLOR/CLICOLOR_FORCE), and --no-color forces it off. (Color for status/log/branch is a tracked follow-up.) -w/--ignore-all-space ignores whitespace when comparing lines (a line that differs from its counterpart only in whitespace shows as unchanged context — like git); -b/--ignore-space-change is the weaker form, where differing amounts of whitespace compare equal but a line with whitespace where the other side has none still differs. -w wins 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 own HEAD, index, in-progress-operation state, and stash (note: git shares the stash across worktrees; mkit's stash is deliberately per-tree). add with 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, and branch -m all refuse with already checked out at '<path>', naming the holding tree (branch moves are single-writer by design; see SPEC-HISTORY-PROOF). remove refuses when the tree has local changes, untracked files, or an operation in progress unless --force is given, and never removes the main tree or the tree you are standing in. prune deletes registry entries whose tree has vanished (--dry-run previews). On-disk: a linked tree's .mkit is a pointer file (mkitdir: <path>) into the main repository's .mkit/worktrees/<id>/. Interim limit: mkit gc refuses 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. apply restores an entry without removing it; clear drops every entry. pop/apply/drop/show select an entry by index (default 0), accepting either a bare N or the stash@{N} form printed by stash list. pop/apply accept --index, which also restores the staged state recorded when the stash was created (like git stash pop --index), not just the worktree changes. save records 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 --index restores 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 --index is a no-op for them (with a note). --format=json (a global flag, valid after any subcommand) emits a machine-readable result to stdout: on list, JSONL with keys index, hash, message (mirroring branch --format=json); on save/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 by mkit add / mkit rm; commit with an empty index is an error. Use mkit add . to snapshot the whole worktree before committing. -m supplies the message inline; -F/--file reads it from <file> (like git commit -F), with - meaning standard input — -m and -F are mutually exclusive, and omitting both launches $EDITOR. -a / --all follows 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 — above user.identity config and the signing-key-derived default). The <spec> grammar is ed25519:<hex> (32-byte public key), did:key:<multibase> (the multibase payload after did:key:, for example did:key:z6Mk…, stored verbatim — must be a non-empty printable-ASCII multibase string), or opaque:<bytes> (raw UTF-8); note this differs from the user.identity config grammar (which also accepts mid:<N> and raw encoded hex). The signature is always produced by your signing key regardless of the recorded author. --amend replaces HEAD instead of adding a new commit: the new commit re-uses HEAD's parent(s) as its own parent(s), takes its tree from the index, and is re-signed; the branch is moved to it and the canonical ancestry snapshot is updated when history proofs are enabled. Without -m the 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 by mkit gc once it falls out of the retention window. After mkit merge --no-commit (or a merge you have just resolved), commit notices the recorded MERGE_HEAD and records a two-parent merge commit rather than an ordinary single-parent one. On success commit prints git's summary on stderr: [<branch> <hash>] <subject> (with (root-commit) for the first commit, detached HEAD when detached) followed by a diffstat and create/delete mode lines; -q/--quiet suppresses it. -S/--gpg-sign[=<keyid>] and --no-verify are accepted no-ops (mkit always signs and has no hooks); --no-edit matches mkit's default amend behavior. --format=json additionally 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 at HEAD; an optional <rev> starts it there instead, a range <A>..<B> shows commits reachable from B but not from A (an empty side means HEAD, so A.. is A..HEAD and ..B is HEAD..B), and a symmetric range <A>...<B> shows commits reachable from A or B but 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 form YYYY-MM-DD HH:MM:SS +0000. --oneline condenses each commit to <abbrev-hex> <title>. --abbrev-commit abbreviates the commit id in the default format too, and --abbrev[=N] sets the abbreviation length (bare --abbrev and --oneline default 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 same N. --format=json emits JSONL (one JSON object per commit, newest first) with keys hash, parents, tree, author, timestamp, title, message; the timestamp stays a raw Unix-seconds integer for machine consumption. --graph is 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 — --author against both the short display form (the Author: line) and the full kind:hex/mid:N form (--format=json's author field); --grep against the full commit message (title plus body). This is intentionally not a regex: unlike git, mkit identities are opaque (Ed25519 keys, mid:N numbers, DID keys), not free-text Name <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), and YYYY-MM-DD HH:MM:SS / YYYY-MM-DDTHH:MM:SS[Z] — a small, explicit grammar, not git's full natural-language approxidate. --no-merges hides merge commits (more than one parent) from the output without changing the walk; --first-parent is 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 -n caps the filtered result, matching git.
  • mkit reflog [<ref>] [--format=json] [-n N] — read-only view of a branch's first-parent ancestry. Defaults to the branch HEAD points at; detached HEAD needs an explicit <ref>. Walks newest-first, addressing each entry <ref>@{N} (@{0} is the current tip). With --features history-mmr, inclusion proofs are checked against the current canonical snapshot and its descriptor, which binds repository, branch, generation, tip, count and root. --format=json emits JSONL with keys ref, selector, index, hash, title, ancestry_verified (true/false/null). Superseded commits after amend/reset are outside this reachable chain; recovery records are separate.
  • 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. --porcelain emits 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, a boundary marker on a file-history root, and filename) once per commit, with each content line tab-prefixed; --line-porcelain repeats the full block for every line. mkit's documented divergences (consistent with --format=json and the log precedent): object ids are 64-hex; author/committer carry mkit's Identity string, not Name <email> (author-mail/committer-mail are empty and both *-tz are +0000, since mkit commits store a single UTC author plus timestamp); filename is the -C copy source on a cross-file copy; git's out-of-scope previous line is not emitted. -w ignores whitespace when matching lines across revisions (like git 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. -M detects lines moved within the file and -C lines copied from other files (like git blame -M/-C; -C implies -M, and repeating it widens the search: -C covers files changed in the commit, -C -C every file in the commit that creates the blamed file, and -C -C -C every 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 -w a 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 -C level (so -C40 -C is 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 -M source, and its -C copy 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 -M source, and with -C -C its 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. -M and --ignore-rev treat every relevant parent uniformly (first-parent-wins); under --first-parent all 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, like git 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-rev accepts any revision (short hash, ref, HEAD~2); --ignore-revs-file entries 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-revs file or a blame.ignoreRevsFile config key; pass the file explicitly.) --ignore-rev-precise (mkit-only; requires --ignore-rev or --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-rev fall-through stays byte-identical to git; see "Divergences from Git" below. Blame is merge-aware by default (like git 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 (like git 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. -L restricts 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, matching git blame -L. An optional <rev> (a ref, hash, or HEAD~2-style spec) blames the file as of that revision instead of HEAD. --reverse <start>..<end> walks history forward instead of backward (like git 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> to HEAD; an explicit <start> is required (mkit reports a clear error for a missing range or open start, where git prints a cryptic "dig up from" message — a deliberate divergence, like the -L diagnostics). 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). -w and -L compose with --reverse. Default emits <short12>\t<line_num>\t<text> per line; --format=json emits JSONL with keys hash, 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, or HEAD; a tag name resolves to its annotated-tag object when one exists. By itself this only proves the object's own embedded signer produced the signature — it does NOT check signer against 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-check signer against the trust-roots registry mkit trust manages, failing closed (nonzero exit, distinct from a bad-signature failure) on an unlisted signer. --trust-roots defaults to $XDG_CONFIG_HOME/mkit/trust-roots.toml; an in-repo path is refused unless passed explicitly (see mkit trust below and docs/THREAT-MODEL.md §5). A verified signature proves the embedded signer produced the commit; it does not bind that signer to a person or organization.
  • mkit prove <revision> [<path>] [--chunk N | --range OFFSET:LEN] [--with-offsets] [-o FILE] [--format=json] — build a disclosure bundle proving that <path> (repository-relative, split on /; omitted: the root tree), chunk N of a ChunkedBlob, or byte range OFFSET:LEN belongs to <revision> (a commit or remix). --chunk and --range are mutually exclusive; --with-offsets is only valid with --range. LEN must be greater than 0. Writes the bundle to -o FILE, or to stdout when stdout is not a TTY (refuses to write binary to a TTY). Default status line: proof: <bytes> B for <path> @ <short id> (<kind>) (stderr when the bundle goes to stdout). --format=json emits {commit_id, path, selector, bundle_bytes, bundle_blake3, output}. Missing path/object exits 66 (NOINPUT); a range that crosses a chunk boundary or an invalid selector exits 65 (DATAERR).
  • mkit verify-proof <commit-id> <bundle-file|-> [--expect-path PATH] [--trusted] [--trust-roots PATH] [--format=json] [--payload-out FILE] — verify a disclosure bundle against a trusted 64-hex commit id. <commit-id> is not resolved as a revision. - reads the bundle from stdin. --expect-path compares the authenticated path to PATH and fails with 65 (DATAERR) on mismatch — the SPEC-DISCLOSURE rule that the caller compares the returned path. --trusted / --trust-roots reuse mkit verify's trust-roots flow against Disclosed.signer; signature_valid == false is reported and, with --trusted, is a failure. A verified bundle proves the content belongs to the commit id; binding the signer to a person or org stays application policy (trust roots). Default output mirrors mkit verify: ok: <kind> <path> @ <short id>, <bytes> B, signer <keyid> (valid|INVALID signature)[, signer trusted] or bad: <reason>. --format=json matches mkit-wasm's verify_disclosure shape plus signer_trusted (true/false/null), including step_inner_roots (hex array) and chunk_inner_root (hex or null). --payload-out FILE writes the verified payload bytes.
  • 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 (like git cat-file). -t prints the type (blob/tree/commit/tag; mkit's remix is the one non-git type); -s prints the size (the content byte length for blobs — matching git — but mkit's serialized size for trees/commits, which differs); -p pretty-prints (a blob's raw bytes, a tree as <mode> <type> <hash>\t<name> lines, or a readable commit/tag summary). --batch reads 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> missing for 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 (like git show), defaulting to HEAD. A commit/remix prints a header (commit <hash>, Author:, Date:, message indented four spaces — matching mkit log) followed by the unified diff against its first parent; that diff body is produced by the same code as mkit diff, so show <commit> is byte-identical to diff <parent> <commit> (modulo abbreviated index ids). --stat shows a diffstat instead of the full patch for commit/remix objects (like git 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 with log, the commit/tag header carries mkit's signed Identity and 64-hex BLAKE3 ids, so those header lines diverge from git's Author: Name <email> / 40-hex form (the same documented divergence as log); 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 modes 100644/100755/120000/040000; the hash is 64-hex BLAKE3). -r recurses, showing leaf blobs with full paths and omitting tree lines (like git); -z NUL-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 (like git 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 always 0 — mkit has no merge stages). --others lists untracked worktree files instead; --exclude-standard drops ignored ones (per .gitignore/.mkitignore) and --ignored inverts to show only ignored files (--ignored requires --others, like git's -i outside an -o/-c selection). -z NUL-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 HEAD prints the current branch; --verify is accepted for git-script compatibility but is a no-op — mkit always errors on an unresolvable revision, with or without it; --show-toplevel prints 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; --count prints the number instead. Reuses log'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-ancestor tests 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 shows refs/heads/*, refs/tags/*, and any refs/remotes/* tracking refs; --heads/--tags filter to one namespace.
  • mkit for-each-ref [--format=<fmt>] [<pattern>...] — iterate refs (heads, tags, and remote-tracking refs), sorted by ref name (like git for-each-ref). The default line is <objectname> <objecttype>\t<refname>. --format substitutes %(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 only HEAD), like git 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 under refs/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 under refs/heads/ or refs/tags/ (other namespaces are rejected), like git 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). -d deletes 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. Covers refs/heads/*, refs/tags/*, and refs/remotes/*/* — the same read scope as show-ref/for-each-ref. An empty repo (no commits yet) prints nothing and still exits 0. --pattern filters to full ref names matching a shell glob (* spans /, ? matches one char, [...] is a character class — the same matcher as branch --list/tag -l). This is the stable inspection surface for refs (#652): scripts should use it instead of ls/cat on .mkit/refs/, which is not a supported interface.
  • mkit ref cat <name> — print the resolved hash for exactly one ref, following HEAD's symbolic indirection (HEAD is 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 literal HEAD; these are exactly the names ref list prints, 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 as subject and 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, or p256. Defaults to attest.default_algorithm in config, else ed25519.
    • --signer <kind> — repo-key (default), keystore, or external. 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 REPLACES attest.external_signer_args from config.
    • --additional-signer "<spec>" — repeatable; adds a second (or third, …) signature to the envelope. <spec> is a comma-separated key=value list: algorithm=<alg>,signer=<kind> [,path=<file-or-binary>][,args=<a>|<b>|<c>]. Here signer= accepts only repo-key or external (not keystore) — unlike the top-level --signer, which also takes keystore. 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's keyid up 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 JSON signatures list is unfiltered (it always carries every signature, independent of --algorithm, which only filters the stderr report). Exits 0 iff 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 with keyid, kind, pubkey_hex). Defaults to $XDG_CONFIG_HOME/mkit/trust-roots.toml. mkit refuses to verify against an in-repo path unless --trust-roots is 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 registry mkit verify --trusted and mkit verify-attest read (same [[trust_root]] TOML file, same default path). add validates pubkey-hex decodes and, if keyid embeds a hex/digest body (the <algorithm>:<hex> or blake3:<hex> shapes), that it matches the given pubkey — a mismatch is rejected, not silently dropped. --kind defaults to ed25519 (commit/remix/tag signing is Ed25519-only today); p256-sec1, secp256k1, and bls12381-thr (with the bls-threshold feature) only matter to verify-attest. add refuses to overwrite an existing keyid without --force; remove requires --yes and fails if the keyid isn't registered. list --json emits 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 (like git branch); * marks the current branch. -v/--verbose adds the abbreviated tip id and commit subject (<marker> <name> <short> <subject>, name column padded like git branch -v); the abbreviated id is a BLAKE3 prefix, not a 40-hex SHA-1 prefix. --format=json on the list form emits JSONL with keys name, current, hash.
  • mkit branch -D <name> — force-delete. mkit does not track per-branch merge status, so -D behaves like -d here: both error on an absent branch (like git 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), like git branch --show-current.
  • mkit branch [--list] [--contains [<c>]] [--no-contains [<c>]] [--merged [<c>]] [--no-merged [<c>]] [<pattern>...] — filter the listing (like git branch). <pattern> arguments are shell globs matched against branch names (*, ?, […]; * spans /, so feature/* works — git's non-pathname wildmatch); a branch is kept if it matches any pattern. Patterns are only treated as filters in list mode — with --list or any ancestry filter present — so mkit 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 to HEAD when omitted (like git). Patterns and ancestry filters combine (a branch must satisfy all of them). --list is an explicit list selector — listing is already the default when no create/delete/rename flag is given, but --list also enables <pattern> filtering (for example, mkit branch --list "release/*", or mkit branch --list main to 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. Prints Switched to branch '<n>', or Already 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; -b refuses to clobber an existing branch, -B create-or-resets. Prints Switched to a new branch '<new>'.
  • mkit switch <branch> / mkit switch -c|-C <new> [<start>] — git's modern branch-switch UX; a thin front-end over checkout with the same clobber guard and output. -c creates (refuses to clobber), -C create-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's clean.requireForce default); -n previews with Would remove <path> lines. Without -d, untracked directories are left alone (git semantics); -d recurses 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 -ff to remove one, which mkit doesn't offer). -x also removes ignored files, -X removes only ignored (-x and -X are 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*'), reusing branch's wildmatch engine.
    • 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, $EDITOR is launched.
    • mkit tag -s <name> [-m <msg>] [<commit>] — create a signed annotated tag: an Ed25519 signature over the canonical tag bytes under the distinct mkit.tag domain. Verify with mkit verify <name>.
    • mkit tag -d <name> — delete a tag.
    • --author <spec> overrides the tagger identity (same grammar as commit --author).
    • --format=json — on the list form, JSONL with keys name, hash, annotated, signed (mirroring branch --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/--message overrides the merge commit message (default Merge branch '<name>'). --no-commit performs the merge and stops before committing: it stages the merged tree and records MERGE_HEAD, then leaves you to finish with mkit commit (which records a two-parent merge commit) or mkit merge --continue. A fast-forward creates no commit, so --no-commit does not affect it. --format=json emits 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-commit applies a clean pick to the index plus worktree without creating a commit (run mkit commit when ready; the result has the current branch as its single parent). A -n pick that conflicts is refused before anything is written — mkit cannot represent a staged-but-unresolved conflict, and a later mkit commit only guards merges, so re-run without -n to use the resumable --continue flow. -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 from git commit -m — git's cherry-pick -m is mainline selection, not a message override. --format=json emits one JSON object to stdout, same shape as merge ("kind":"commit"|"no-commit"|"aborted" on success, "kind":"conflict" with a conflicts array 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 is Revert "<subject>" with a This 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-commit stages 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=json emits the same outcome-object shape as merge/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; --skip drops the current commit. See "Resolving conflicts" below. -i/--interactive opens the todo list in $EDITOR before any mutation. Each line is <command> <commit> <subject>, oldest-first (top is applied first). Reorder lines to reorder commits; delete a line (or use drop/d) to drop a commit; reword/r edits a commit's message when it is replayed; squash/s folds a commit into the previous one and combines their messages (via the editor); fixup/f folds in but keeps the previous message. A squash/fixup cannot be the first line (nothing to fold into) and is rejected before any mutation. Removing every line resets the branch to the base. A reword/squash whose replay hits a conflict still reopens the editor on --continue. edit (stop-to-amend) is not yet supported. --format=json emits 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. run drives the loop automatically: it checks out each candidate, runs <cmd>, and classifies from the exit status (git's contract: 0=good, 125=skip, 1–127 else=bad, ≥128=abort), then restores the original HEAD and prints the first bad commit. The candidate hash is also exported as MKIT_BISECT_COMMIT. (mkit's bisect is otherwise print-candidate — start/good/bad/skip print the next candidate rather than checking it out — so run checks 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 (a Cargo.lock refresh, a regenerated snapshot) doesn't block the next checkout. If every remaining candidate is skipped (exit 125), run reports 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 repo run materializes 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 by commit --amend/reset/rebase), then deletes unreachable objects older than the grace window (default 14 days). -n/--dry-run reports what would be pruned without deleting. --grace-secs 0 prunes every unreachable object, but bypasses the grace window — the grace window is gc's concurrency-safety net (some root-publishing paths such as tag and fetch write an object before publishing the ref that makes it reachable), so --grace-secs 0 must 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. See docs/specs/SPEC-GC.md.

Resolving conflicts

merge, cherry-pick, and rebase all share one resumable-conflict workflow. A path where both sides changed the same regular file on disjoint lines is auto-merged line-by-line (the union of both edits, no markers) — changes on different lines merge cleanly even when adjacent. A conflict is only raised when the two sides' changed regions overlap (modify the same or interleaving lines), or for binary files. When a 3-way merge cannot auto-resolve a path, the command:

  1. 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.
  2. 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.

Operation-state files

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).

FileMeaning
MERGE_HEADother parent of an in-progress merge (presence ⇒ merge)
CHERRY_PICK_HEADcommit being applied by an in-progress cherry-pick
ORIG_HEADHEAD before the operation, used by --abort
MERGE_MSG / CHERRY_PICK_MSGpending commit message
mkit-conflictsmkit 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. Bare remote prints one remote name per line (git-shaped); -v/--verbose adds the URL and direction (<name>\t<url> (fetch) / (push)). --format=json emits 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 default remote_endpoint. The URL MUST start with mkit+<scheme>:// (see below).
  • mkit remote set [<name>] <url> — alias for mkit remote add.
  • mkit remote remove <name> (alias rm) — delete a named remote. The reserved name default clears the flat remote_endpoint.
  • mkit remote rename <old> <new> (alias mv) — rename a named remote and repoint any branch.<b>.remote upstream tracking it. Refuses to clobber an existing <new>. Removing or renaming a remote never touches the user-scoped trusted_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 (use default for the flat default remote). mkit remote set-url <name> <url> — change an existing remote's URL (validated like remote 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 --sparse takes one or more PATTERN arguments, whereas git's clone --sparse is a boolean flag (it cones to the top-level files and you add patterns afterward). --depth N is parsed but not yet wired: passing it fails with a clear --depth is not yet wired usage 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 -b never 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/config instead of the implicit flat default remote (mirrors mkit remote add <name> <url> at clone time). The name must be dot-free and ref-safe, like a named remote add. Tracking refs land under refs/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 under refs/remotes/<remote>/<branch> (refs/remotes/default/<branch> for the flat default remote) and do not move local branches. --all fetches every configured remote (the flat default plus every named remote.<name>.url) in one invocation, printing each remote's usual From <url> plus per-ref summary in turn; mutually exclusive with an explicit <remote> argument. A per-remote failure does not stop the others — fetch --all continues through the full list and exits non-zero if any remote failed. --format=json emits 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 from refs/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. --all pulls from every configured remote in turn (same enumeration and failure-tolerance as fetch --all), fast-forwarding the current branch from each; mutually exclusive with an explicit <remote> argument. --format=json emits 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/fetch verify every commit/remix/tag the fetch newly introduces — the same check mkit 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-scoped pull.require_signed = false config key — it cannot be set from a cloned repo's own .mkit/config (see THREAT-MODEL.md §4). --all applies the same verification (and the same opt-out) to every remote it iterates.
  • Library. clone, fetch and pull run on the mkit-sync crate (rust/crates/mkit-sync/README.md), which an embedder can call directly.
  • 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 (the branch.<b>.remote/.merge tracking 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. --all mirrors every refs/heads/* (also CAS-safe). --force overwrites the remote branch unconditionally (skips CAS); --force-with-lease overwrites only if the remote hasn't moved past the last-seen tip (the two are mutually exclusive); -f is an alias of --force. -u/--set-upstream records the pushed remote as the branch's upstream even if one was already set. --dry-run resolves 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 success push prints git's To <url> plus ref-update summary (or Everything up-to-date). --format=json emits one JSON object to stdout: {"ok":true,"remote":"...","endpoint":"...","branch":"...", "remote_branch":"...","old":"<hex>|null","new":"<hex>","forced":<bool>, "up_to_date":<bool>,"steps":<n>} on success (steps is 0 when up to date, otherwise the number of branch advances the push took; --all reports the total plus ref_count), or {"ok":false,"rejected":true, "branch":"...","error":"..."} on a non-fast-forward (CAS) rejection ({"ok":false,"error":"..."} for any other failure). On a V2 Connect remote, pushes at or above its advertised upload threshold open a signed ticket for each data pack and packmap node, then consume those tickets with the branch advance. A single advance can carry six data packs. A push that needs more is split automatically along the branch's first-parent history into several advances, each moving the branch to an intermediate commit whose closure fits; nothing is uploaded ahead of its advance. Every intermediate state is published: the remote branch, its Stage 2 hooks, receipts and lifecycle events all fire once per advance, and a failed push leaves the branch at the last published commit, which the error names. Run mkit push again to resume from there (the remote-tracking ref follows each advance). The first advance uses your usual lease (or --force); every later one is a compare-and-swap on the previous advance's commit, even under --force. Before uploading anything, the client checks that your stored write grants cover every advance: a non-owner key needs c to create the branch (or u/f to update it), and an f grant for the later advances, on every server until indexed-mode u-only fast-forwards land (R-148); the owner key needs no grant. A push that would be refused midway is refused up front, with no advance published. A single commit or merge that cannot be split (a merge always lands whole) and still needs more than six packs is refused before any upload, naming the commit; ask the operator to raise max_pack_bytes. On a terminal the progress line reads Writing objects (step 2/5); piped, a pushed step 2/5: branch now at <hex> line follows each advance; --quiet prints neither. Large packs upload in parts. Receipt files in the repository's common upload-parts/ cache let the next mkit push resume the parts already accepted, provided the regenerated push plan is identical. After an interruption, run mkit push again to resume; the message reports how many parts were saved. Legacy Connect remotes use the existing upload path.
  • Transfer progress (#711). clone/push/pull/fetch stream a live, honest progress line on stderr while the network transfer runs — Writing objects: N objects, B bytes while building/uploading the outgoing pack, Unpacking objects: N objects while applying a downloaded one. The counts are real (objects actually staged/unpacked, bytes actually handed to the transport); mkit never fabricates git's Enumerating/Counting/Compressing objects or Total 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/--quiet forces it off, and MKIT_PROGRESS=always/never overrides the tty-detection explicitly (mirrors NO_COLOR/CLICOLOR_FORCE).
  • mkit serve [--idle-timeout-secs <secs>] [--max-session-secs <secs>] <path> — internal SSH transport server. Speaks the mkit-rpc SSH framing on stdin/stdout (its only mode). mkit serve --root <dir> [--principal <hex>] [<ns>/<name>] is the multi-repository form a forge forces: the path (or, when it is omitted, an SSH_ORIGINAL_COMMAND of exactly mkit serve <path>) names a <NAMESPACE>/<NAME> resolved under <dir> — a bare name, an uppercase byte, .., an extra component or a symlinked component is refused — and only the namespace's Ed25519 owner may write. --principal (a raw 32-byte key as 64 lowercase hex) is the sshd configuration's trust assertion of the caller's key, read from argv alone and never from the environment; a session without it is read-only. Packs a session uploads and verifies may be published by that session's packmap write (at most seven between packmap writes); a packmap whose node's prev is neither absent nor the value the write replaces, whose node or a listed pack is neither pending nor a member (a pending packlist listed as a pack counts as refused), or that lists more than 1,024 packs is refused. See SPEC-TRANSPORT §4.1 and SSH-SECURITY.md §5. Both forms hold a shared serve.lock in <path>/.mkit for as long as the process is alive (any number of concurrent serve processes may hold it at once); a local worktree-mutating command or gc run against that same path while it is held prints a warning to stderr and proceeds — it is not refused, and this is detection, not coordination (SPEC-CONCURRENCY §3.1).

    --idle-timeout-secs <secs> (default 60; 0 disables it) ends the session after that long without a byte from the client, whether before the handshake, between requests or in the middle of an upload (whose partial pack is discarded): it answers Error{INVALID_REQUEST, "idle timeout"} and exits 76. An upload that keeps sending never trips it, however slow. It bounds only a silent client: one that trickles bytes, or stops reading a download, is not idle. For those, --max-session-secs <secs> (default 0, off) ends the process that long after it starts, whatever the client is doing, exit 76; set it above your longest legitimate clone or push. Both flags accept at most 604800 (7 days). See SSH-SECURITY.md §4.

    Refs are served only under refs/ (SPEC-REFS §2). A read or write of any other name is refused by name ("ref name must start with refs/ …"); see "Refs outside refs/" below. At startup, when no other mkit serve or server process is serving the root, it also removes upload temp files (packs/.<hex>.tmp.<pid>.<seq>) that a crashed server left, once they are an hour old. A root marked .mkit/server-meta (served from SQLite metadata by a server) is refused (exit 78): its refs live in the database.

    mkit serve has no network listener, and --http and --listen-enc are usage errors (exit 64). Serve repositories over SSH with mkit serve, or embed mkit-server to expose Connect over HTTP. The mkit+enc:// transport is deprecated: no maintained server exists for it (a mkit built with --features enc-transport still carries the client). MKIT_API_TOKEN is sent as a bearer token by mkit+https:// remotes.

    Refs outside refs/. mkit serve before 0.5 stored any grammar-valid ref name as a file at the served root, so a name without the refs/ prefix (main, heads/main) became <path>/main or <path>/heads/main. No mkit client ever wrote such names, but a third-party client could have. Servers now serve only refs/ names (SPEC-REFS §2) and refuse others; they never read, move or delete these files. To find candidates, with no server running against the root, list the small files outside .mkit/, refs/ and packs/ that hold a 64-hex ref id, and review each one (a worktree file can match too):

    cd /srv/mkit/repo
    find . -path ./.mkit -prune -o -path ./refs -prune -o -path ./packs -prune \
      -o -type f -size -81c -exec grep -lxE '[0-9a-f]{64}' {} +

    Then move each real ref to the refs/ name your clients use, without overwriting an existing one (for a branch main, refs/heads/main):

    mkdir -p refs/heads && mv -n main refs/heads/main

    or delete it if nothing needs it. The same applies to a root served by a mkit-server fs-layout deployment with the default .mkit-layout refs.

  • 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 closure export <revision> [--history] [-o DIR] [--force] [--format=json] — write a full-disclosure closure of <revision> as DIR/MANIFEST.mkcl plus DIR/<pack_key hex>.pack files (raw-only v1 packs). Default mode is snapshot (the commit and its tree closure; parents referenced but not included); --history exports the full ancestry. Default DIR is ./<short id>.closure/. Refuses to write into a non-empty directory without --force. This is not mkit git export, which publishes refs to a git mirror; closure export is mkit's native object-set disclosure (nearest git relative: git bundle). Status line: closure: <mode>, <n> objects in <k> pack(s), <total> B -> DIR. JSON: {root, mode, packs: [{file, blake3, bytes}], objects, manifest}.
  • mkit closure verify <commit-id> [--from DIR] [--history] [--show-unreferenced] [--format=json] — verify a closure. With --from DIR, reads MANIFEST.mkcl and every pack it names and checks them against a trusted 64-hex <commit-id> (not a revision). Without --from, checks the local repository (<commit-id> may be a revision; --history selects history vs snapshot); a corrupt on-disk object (its bytes no longer hash to its filename) is reported under corrupt, the same as a corrupt object in a served pack — local closure verify is an fsck-shaped check, not a hard read failure. Output: ok: closure complete (<verified> objects, <mode>) or bad: closure incomplete: <m> missing, <c> corrupt plus up to 10 ids each; unreferenced is a note, not a failure, and — local mode only — is hidden unless --show-unreferenced is passed (the local store is expected to be a superset of any single commit, so a long unreferenced list is usually noise; --from always shows it). JSON matches mkit-wasm's ClosureReport shape (root, mode, verified, complete, missing[], corrupt[{id, reason}], unreferenced[], unreferenced_checked); unreferenced is [] in local mode unless --show-unreferenced is passed, and the unchecked streaming mode prints a note explaining how to opt in. Incomplete closures exit 65 (DATAERR).

    The store-less --from/pack mode can check unreferenced objects, while local mode streams only reachable objects by default and enumerates the whole store only with --show-unreferenced.

  • 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-name defaults to mirror, and --ref takes 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 in mkit-* 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-lease from recorded state under .mkit/git/<name>/; mkit-side history rewrites mirror as force-pushes. Unless --no-attest, each exported head gets a signed git-bridge/v1 DSSE attestation (saved like mkit attest, published on the mirror's refs/mkit/attestations ref — fetch it with an explicit refspec; git clones skip it by default). Attestation signing uses the same signer resolution as mkit attest (--algorithm/--signer flags, 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-name state 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 as refs/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 a git-import/v1 attestation 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. Plain git export toward 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). --key points at the import signing key (default: the pinned key file), and --json emits 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 direction fork (sticky); plain export under that name is then refused, while git fetch/pull keep 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 as git 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 git subcommands require building the binary with --features git-bridge (alias: git-export); they shell out to git.

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), signed commit, branch create/checkout, init, keygen (ed25519 commit key, or secp256k1/p256 attestation keys), plus the differentiators — verify (signature check), attest (DSSE; supports algorithm and repo-key/keystore signer), and verify-attest (trust-roots-gated, with an algorithm filter). Every tool takes an explicit repo_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 to attest must resolve inside the repo (no reading outside files into a signed attestation), a trust_roots path passed to verify-attest must resolve outside the repo (a hostile clone's in-repo .mkit/attest-trust-roots.toml can 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 the attest CLI for that flow). initialize must 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

Grants, epochs and repository visibility (SPEC-WRITE-GRANTS):

A grant is a statement signed by a namespace owner that lets a grantee's key read or write repositories in that namespace on a deployment. mkit grant manages the grants you hold; the store is a directory of files under the user config directory. It only holds client-side grants; it does not register grants on a server.

  • mkit grant create --cap CAP --grantee HEX (--repo NAME | --all) [--refs PATTERN=FLAGS]... [--audience ORIGIN]... [--ttl DURATION] [--epoch N | --offline] [--namespace NS] [--remote REMOTE] [--store] — create an owner-signed grant and print its header (give it to the grantee). CAP is read, read,write or write (write,read is accepted and spelled canonically). --refs is pattern=flags with flags from cufd, required with write and not allowed with read (a server without indexed mode cannot check a fast-forward, so it applies an update to an existing ref only under f: use cuf, not cu, for pushes that move a branch); audiences, ref scopes and flags are sorted and deduplicated. --ttl is at most 30d (default 7d). The epoch defaults to the remote's current one (GetGrantEpoch) or 0 with --offline; the audience defaults to the trusted remote's origin. --store also adds it to your own store.
  • mkit grant add [--remote REMOTE | --offline] <file|-> — verify a grant header's owner signature and add it to your grant store. Idempotent on the same grant id. Anything the verifier rejects is refused with the rule named. It then makes one short attempt to ask the trusted (or --remote) deployment for the namespace's epoch and warns when the grant's epoch is above it (see the epoch rule below); if that check fails it warns and stores anyway, and --offline skips it.
  • mkit grant list [--check] [--remote REMOTE] [--json] — id, namespace, scope, capabilities, ref scopes, audiences, epoch, expiry and status (valid, expired, not yet valid). --check asks the trusted (or named) remote for each namespace's epoch and marks stale epoch / future epoch, only for grants whose audiences include that remote (others are unchecked). It never contacts an origin that a grant file merely names.
  • mkit grant revoke <remote> [--namespace NS] [--audience ORIGIN]... [--prune] [--timeout DURATION] — sugar for mkit epoch bump --by 1. It first lists the local grants the bump invalidates, and afterwards prints how to reissue. A grant that also lists an audience the bump does not cover stays valid there: it is listed as "still valid at ..." and kept. --prune deletes only the grants every audience of which the bump covers, once it succeeds.
  • mkit epoch show <remote> [--namespace NS] [--json] — the epoch the remote stores for a namespace (0 if never set).
  • mkit epoch bump <remote> [--by N] [--namespace NS] [--audience ORIGIN]... [--timeout DURATION] [--json] — raise the epoch by N (1 to 1024, default 1), revoking every grant issued at a lower epoch. The statement is signed once. While the server answers unavailable with Retry-After, bump waits (1 to 60 s per wait) and re-sends the identical statement, up to --timeout (default 5m). Ctrl-C cancels and says the bump may still finish on the server; check mkit epoch show before running it again, because a new run signs a new statement and would raise the epoch again.
  • mkit visibility set <remote> public|private [--statement] [--audience ORIGIN]... [--timeout DURATION] — switch a repository between public and private. By default it is a signed request with the repository signing key (transport_auth = envelope, a trusted remote, and the owner's key). With --statement it sends an owner-signed mkit-repo-visibility:v1 statement with no envelope, which any owner scheme can sign.

Cloning a private repository: a Connect clone with transport_auth = envelope signs with signing_key, which defaults to the repository-relative .mkit/keys/default.key. A fresh clone has no such file, so set a user-level signing_key (or signer = keystore) before cloning.

Signing flags (grant create, epoch bump, grant revoke, visibility set --statement): the owner signs with the mkit signing key (ed25519, the default; the namespace is ed25519-<pubkey>) or, with --scheme secp256k1-eip191, with a software-keystore secp256k1 key (key.secp256k1_ref; the namespace is its 0x address). A wallet or authenticator signs by import: --print-statement writes the exact statement bytes to stdout (the digest or challenge to sign goes to stderr); then rerun with --statement-file <that file> and either --signature <hex> (65 bytes r‖s‖v, v of 0, 1, 27 or 28, a high s is normalized) or --webauthn-assertion <file> (JSON with base64url publicKey (x‖y), authenticatorData, clientDataJSON and signature, DER or low-s raw). A webauthn-p256 import is refused unless the user config pins the relying party with grant.webauthn_rp = <rp_id> <origin>.... Every header is verified locally before it is stored or sent.

How a held grant is used: mkit push, pull, fetch and clone attach the best grant from your store to each signed request, for the key that signs them. Among the grants valid for the request, the higher epoch wins, then the most specific repository and ref scope, then the latest expiry, then the greater header bytes. A grant pre-issued for epoch e+1 therefore outranks a live epoch-e grant until the owner raises the epoch. A write-only grant never implies read on a private repository (only GetReceipt accepts it). Each signed read may prompt for a signature when your key needs a touch or a passphrase. Epochs are per deployment, so a grant lists the audiences it is valid at. The grant store directory is never followed through a symlink: if $XDG_CONFIG_HOME/mkit/grants is a symlink, mkit refuses it rather than trust where it points. Files are read without following symlinks and only files named <64 hex>.grant are considered.

Config / keys / version:

  • 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. ALG is ed25519, secp256k1, or p256; default is ed25519.
  • 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=json emits 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 a set of 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 a REPO_FORBIDDEN_KEYS key — unless overridden by --local/--global. Idempotent: unsetting an already-absent key is a silent success (like rm -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. --local forces the repo-scoped layer (refused for a REPO_FORBIDDEN_KEYS key, which can never live in a clone-traveling repo config); --global forces the user-scoped layer even for an otherwise repo-safe key. Put --local/--global before --unset (mkit config --global --unset <key>, not --unset --global <key>) — like any clap value-taking option, --unset consumes the very next token as <KEY>, so a flag placed between --unset and 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 in REPO_FORBIDDEN_KEYS.
  • mkit config <key> <value> — set a configuration value. Config section and variable names are matched case-insensitively (canonicalized to lowercase), like git — so user.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 of remote.<name>.url or branch.<branch>.remote — stay case-sensitive (git semantics), so a remote added as Origin is not silently folded to origin.
    • user.name / user.email — git-compatibility aliases. They are accepted and round-trip like git config user.name, but are non-authoritative: mkit's commit author is cryptographic (user.identity or 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 — unlike user.identity, which stays user-scoped and in REPO_FORBIDDEN_KEYS so a hostile clone cannot spoof authorship. The first time either alias is set in a repo that has never set user.identity (or the other alias), mkit config prints a one-time warning to stderr pointing at user.identity — a user coming from git might otherwise reasonably expect these to control 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. Dangerous core.* 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 GitHub Release. The downloaded archive is checked against its sha256 sidecar asset when the release publishes one; this runs entirely in-process, no cosign and no GitHub attestation API are involved. Only installer-managed binaries are updated: the .mkit-installed-tag receipt written by install.sh must 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 json carries the machine-readable bit (status: up-to-date | update-available).
    • Downgrade policy mirrors install.sh: resolving latest never downgrades; an explicit --version pin may downgrade only with --allow-downgrade, loudly.
    • The staged binary must pass a pre-swap self-check (version must emit exactly mkit <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.
  • mkit version — print the version. Emits exactly mkit <X.Y.Z>\n. The top-level mkit --version / mkit -V flags are aliases of this subcommand and emit the identical string. Note this is mkit <X.Y.Z>, not Git's git version <X.Y.Z> form — an intentional, documented divergence (the string is pinned by packaging asserts).

Ignore files

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 (*.log matches a/b/c.log). A pattern containing a / — including a leading / — is anchored to the repo root (/foo matches only the top-level foo; src/gen only 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.
  • .mkit and .git are 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.

Divergences from Git

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, so mkit status and 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/fetch do stream an honest Writing objects: N objects, B bytes / Unpacking objects: N objects progress line (#711) — real counts only; see "Transfer progress" above.
  • mkit remote lists remote names only (git-shaped); use mkit remote -v for <name>\t<url> (fetch)/(push).
  • mkit log --graph is 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 default log body already diverges from git's commit/Author/Date (mkit Identity plus 64-hex ids); an --oneline --graph renderer for the linear/DAG case is a tracked post-v1 follow-up, not a v1 blocker.
  • mkit log --author/--grep are plain substring matches, not regexes. git's --author/--grep default to POSIX extended-regex matching against a free-text Name <email> header; mkit identities are opaque (Ed25519 keys, mid:N numbers, 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/--until use a small, explicit date grammar, not git's approxidate. Accepted forms: @<unix-seconds>, now/today/yesterday, <N> <unit> ago, YYYY-MM-DD, and YYYY-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=v2 matches git's format except that object ids are full 64-hex BLAKE3 (git's are 40-hex SHA-1). Exact renames emit a 2 record (R100); --branch header lines are not emitted.
  • mkit clone --depth N is parsed but not yet wired. The flag is accepted by the parser but clone rejects it with a --depth is not yet wired usage 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-rev fall-through, not git's positional guess. git's --ignore-rev pairs 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-rev fall-through is unchanged and stays byte-identical to git; --ignore-rev-precise must be passed explicitly (and requires --ignore-rev/--ignore-revs-file).
  • mkit reset [--soft|--mixed|--hard] [-f] [<commit>] — --soft moves HEAD only; --mixed (the default) also resets the index; both leave the worktree untouched. --hard additionally 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, --hard refuses (without -f/--force) when discarding would lose locally-modified or staged content, or overwrite such a colliding untracked path; with -f it is overwritten. git's reset --hard discards 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 to HEAD.

Commands deliberately not implemented (by design)

Several Git commands that were once "wontfix-by-design" have been reinstated under safety guards as part of the git-parity porcelain-completion work (#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.

Config keys

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.*, trusted_remote_endpoint, admission_helper, grant.webauthn_rp, and remote.<name>.admission_headers — 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.

KeyValueDefaultNotes
user.identityhex IdentityunsetUser-scoped only; see below
signing_keypath.mkit/keys/default.keyEd25519 seed file; user-scoped only
default_branchnamemainBranch for mkit init
remote_endpointURL / pathemptySet via mkit remote add
remote_bucketnameemptyFor s3 remotes
remote_typefile / http / s3 / ssh / memoryauto
transport_authbearer / envelopebearerWrite-auth mode for mkit+https:///mkit+http://; envelope additionally Ed25519-signs writes with the commit-signing key (see signer/signing_key/key.ed25519_ref)
http.sslCAInfoPEM certificate file pathunsetAdditional trust certificates for native Connect HTTPS remotes only; normal user/repository/-c configuration scopes apply. MKIT_SSL_CA_FILE takes precedence. See HTTPS certificate trust.
admission_helperabsolute executable pathunsetUser-scoped; invoked once for a 402 on a trusted remote. Its stdin includes untrusted server challenge content. The helper decides whether and how much to spend.
remote.<name>.admission_headerscomma-separated HTTP header namesemptyUser-scoped additions to the admission helper's request allowlist; hard-reserved names are refused.
ssh.strict_host_key_checkingyes / no / accept-newinheritUser-scoped only
ssh.user_known_hosts_filepathinheritUser-scoped only
ssh.identity_filepathinheritUser-scoped only
signerlegacy / keystorelegacyUser-scoped commit signing source
key.backendbackend namesoftwareUser-scoped default for mkit key
key.default_ref<backend>:<label>software:defaultUser-scoped fallback key ref
key.ed25519_ref<backend>:<label>software:defaultUser-scoped Ed25519 ref
key.secp256k1_ref<backend>:<label>software:default-secp256k1User-scoped secp256k1 ref
key.p256_ref<backend>:<label>software:default-p256User-scoped P-256 ref
attest.signerrepo-key / keystore / externalrepo-keyUser-scoped attestation signer
grant.webauthn_rp<rp_id> <origin>... (repeatable; | separates entries in one value)unsetUser-scoped only. Pins the WebAuthn relying parties mkit grant/epoch/visibility accept for webauthn-p256 signatures; unset refuses them

Keystore backend names include software, software-raw, macos-keychain, linux-secret-service, systemd-creds, and yubikey when the target build enables the corresponding backend feature. Security- sensitive selector keys are ignored from repo-local config; set them in $XDG_CONFIG_HOME/mkit/config or with explicit command flags.

HTTPS certificate trust

For a private or local mkit remote over Connect HTTPS (mkit+https://), supply a PEM file containing the CA certificates you trust:

mkit config http.sslCAInfo /absolute/path/to/local-ca.pem
# Override the configured file for one command:
MKIT_SSL_CA_FILE=/absolute/path/to/other-ca.pem mkit fetch origin

http.sslCAInfo follows the normal user, repository, and command-line -c configuration precedence. MKIT_SSL_CA_FILE overrides the resulting value. Configured relative paths start at the repository root; ~/ expands against HOME. An environment file path is used as supplied. The selected file adds certificates to the bundled Mozilla roots; certificate-chain and hostname verification remain enabled. A missing, unreadable, empty, or malformed selected file is a hard error. Keep private keys out of the CA file; it contains certificates only.

These settings apply only to native mkit remotes over the Connect transport: HTTPS RPCs, pack uploads and downloads. The self-updater uses the OS trust store and the release checksum when present; it does not read MKIT_SSL_CA_FILE / http.sslCAInfo. S3 remotes do not yet honor either setting; private-CA S3 endpoints are a post-launch follow-up. Wasm/browser certificate trust is unchanged. The key shares its spelling with Git's CA-file setting; mkit uses its own MKIT_SSL_CA_FILE environment variable.

Admission helper

The admission helper's stdin carries raw headers["www-authenticate"] field values. One value can contain several challenges when the server's platform combines repeated fields. Helpers MUST parse each value as an RFC 9110 §11.6.1 challenge list, preserve challenge order, and distinguish list separators from commas within auth-params and quoted strings. The separate typed challenges array remains available. mkit forwards the raw fields without splitting them.

user.identity

The commit author Identity, encoded as [kind:u8][len:u16 LE][bytes] in lowercase hex per docs/specs/SPEC-OBJECTS.md §9. Accepted shorthands at parse time:

user.identity = ed25519:<64-char-hex>   # kind=0x01, 32-byte pubkey
user.identity = mid:<u64 decimal>       # kind=0x03, 8-byte LE opaque
user.identity = <raw-hex>               # already-encoded Identity

When unset, the CLI derives an Ed25519 Identity from the signing key's public key at commit time. Most users don't need to set this.

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.

ssh.*

Thin overrides for the ssh child process spawned by the SSH transport. Empty string ("") means "do not pass the flag; inherit the user's ~/.ssh/config default". See docs/SSH-SECURITY.md for the recommended trust model.

URL schemes

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:

SchemeFormUse
mkit+filemkit+file:///abs/pathlocal filesystem mirror
mkit+httpsmkit+https://host[:port][/path]ConnectRPC gateway (for example, VCS Worker)
mkit+s3mkit+s3://endpoint/bucket[/prefix]S3-compatible object store
mkit+sshmkit+ssh://user@host[:port]:pathSSH with the mkit shell
mkit+memorymkit+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, an auth-v2 mkit-server deployment) need mkit config transport_auth envelope — see the config table above and SPEC-TRANSPORT-CONNECT §7.3.

The URL path names the repository as <namespace>/<name> (SPEC-TRANSPORT-CONNECT §7.4). Two namespace grammars are accepted: the self-certifying ed25519-<64 lowercase hex> and 0x<40 lowercase hex> forms, and the opaque authority-mode form [a-z0-9][a-z0-9._-]{0,71} other than root, ed25519-… and 0x… (SPEC-SERVER §6.2.2). An authority-mode deployment can therefore use a UUIDv7, for example mkit+https://vcs.example/019c88c3-a904-7bd1-8a5d-3182a0c6978a/repo. The URL does not say which mode the server runs; the server still enforces only its own grammar and answers invalid_argument for the other. Plain https:// is not a sync remote — at a server's origin it serves objects by repository path (SPEC-HTTP-OBJECTS) — so mkit clone https://… is refused with a hint to add the mkit+ prefix.

Private reads on an authority-mode server are decided by the deployment's authority hook on the signer's key, not by grants or owner statements. Read one by signing each request: run mkit config transport_auth envelope and mkit config trusted_remote_endpoint <url>, with a user-scoped signing_key as for any private clone. To a caller the hook does not admit, a private repository answers not_found, indistinguishable from a missing one. MKIT_API_TOKEN is still sent as Authorization: Bearer …, but the server does not pass it to hooks: it discards an Authorization value unless it is a single Payment credential, and an unsigned request captures no credentials at all.

See docs/specs/SPEC-TRANSPORT.md for the other transports' wire protocols.

Version output contract

mkit version prints exactly:

mkit <X.Y.Z>\n

Downstream packagers (Homebrew) assert on this substring. The format is pinned by a snapshot test in the CLI crate.

*nix conventions

mkit follows common POSIX CLI conventions so shell scripts, pipelines, and interactive use behave predictably.

Exit codes

Based on BSD sysexits(3):

CodeConstantMeaning
0okSuccess
1general_errorCatch-all for other failures
64usageWrong args or unknown subcommand
65dataerrMalformed input (corrupt object, bad hash)
66noinputMissing / unreadable input file
69unavailableTransport could not connect
70softwareInternal consistency error (a bug, not bad input)
73cantcreatCannot create output file
75tempfailTemporary failure; retry is safe
76protocol_errorBad URL scheme or malformed server response
77nopermPermission denied
78config_errorUnknown 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.

Environment variables

  • GIT_EDITOR / EDITOR / VISUAL — the editor mkit commit opens when neither -m nor -F is supplied, tried in that order. If none is set it falls back to vi, matching git. In a headless or non-interactive context (CI, an agent), always pass -m/-F so 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_COLOR overrides this.
  • SSH_AUTH_SOCK — standard OpenSSH agent socket, used by mkit+ssh:// transports.
  • MKIT_SSL_CA_FILE — PEM certificate file for additional native Connect HTTPS remote trust only, overriding http.sslCAInfo. The self-updater is excluded by design; S3 remotes are not yet covered. Certificates are added to the Mozilla roots, with chain and hostname verification enabled. Missing, unreadable, empty, or malformed files are hard errors. See HTTPS certificate trust.
  • XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME — XDG Base Directory roots for user-level config, keystore, cache, and state respectively. Defaults per the freedesktop spec (~/.config, ~/.local/share, ~/.cache, ~/.local/state).

File layout

PathPurpose
.mkit/Repo-local state (like .git/)
.mkit/configRepo-local config — overrides user-level values
.mkit/keys/default.keyRepo-local Ed25519 signing key
.mkit/indexStaging index
.mkit/index.lockHeld by commit/checkout/merge/rebase
.mkit/COMMIT_EDITMSGScratch file for mkit commit without -m
$XDG_CONFIG_HOME/mkit/configUser-level config (cross-repo defaults)
$XDG_CONFIG_HOME/mkit/grants/Your grant store: <grant id>.grant files (0700/0600), never repo-scoped
$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.

Signals

  • SIGINT (Ctrl-C) and SIGTERM set a graceful-shutdown flag. Long-running operations (push, pull, clone, log) poll it at natural checkpoints and exit with tempfail (75) so the operation can be retried. Wired via signal-hook's flag module so the CLI stays under its crate-level #![deny(unsafe_code)].
  • SIGPIPE is ignored. Pipelines like mkit log | head -1 exit cleanly without a "Broken pipe" message — the next write just propagates EPIPE as 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).

Man page

# 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 mkit

Homebrew users installing from the tap get the man page automatically.

Shell completions

# 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/_mkit

Then restart your shell (or run compinit on zsh). Completion covers the full subcommand list plus common flags; per-argument completion (branch names, remote URLs) is deferred to a future release.

Using mkit as git (opt-in shim)

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 shim

See 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.

Files