KIT

Reference snapshot · source revision 7a2e36f2b250934a095e542ae20e62dcfb0d5f70

kit — CLI reference

Short user-facing reference for the kit command. For wire-format details see the spec docs in this directory (SPEC-OBJECTS, SPEC-INDEX, SPEC-REFS, SPEC-PACKFILE, SPEC-TRANSPORT, SPEC-SIGNING, SPEC-FASTCDC, SSH-SECURITY).

Quick start

kit init                          # create .kit/ in the current directory
kit keygen                        # generate an Ed25519 signing key
echo hello > README.md
kit add README.md
kit commit -m "first commit"
kit log

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

Global flags

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

--git-dir, --work-tree, and --exec-path are non-goals — kit uses the .kit/ marker and has no split work-tree / exec-path model.

Subcommand reference

Working-tree commands:

History / commits:

Read-only plumbing (object/ref inspection, for scripts and agents):

Attestations:

Branches / refs:

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 .kit/ (see below) and exits non-zero.

To finish, for each conflicting path: edit the worktree file to its resolved content (remove all conflict markers), kit add <path>, then:

kit merge --continue        # or cherry-pick --continue / rebase --continue

--continue refuses while any text-marker file still contains markers, and refuses whenever a conflicted path's worktree resolution does not match what is staged in the index — so an unstaged resolution can't be silently dropped. This covers every shape: an edited regular or executable file, a path deleted without kit rm, and a file replaced by a symlink or directory without kit add. An unchanged conflict (the worktree still equals the auto-staged ours-side, including its executable / symlink mode) needs no re-add and continues. The committed tree is built from the resolved index/worktree — not the conflict-time "ours wins" snapshot — so your edits (including a third distinct resolution) and the merge's clean changes both land.

Alternatively:

kit merge --abort           # restore HEAD, branch ref, index, and worktree
kit rebase --skip           # rebase only: drop the current commit, keep going

--abort restores the pre-operation state (or fails with a clear, recoverable error and changes nothing). Starting a new merge / cherry-pick / rebase while one is already in progress is refused.

Operation-state files

All live under .kit/; rebase keeps its state inside .kit/rebase-apply/. These use Git-compatible names plus one documented kit sidecar. The .kit/index stays a single-stage resolved staging area — there are no unmerged index stages (SPEC-INDEX is unchanged).

File Meaning
MERGE_HEAD other parent of an in-progress merge (presence ⇒ merge)
CHERRY_PICK_HEAD commit being applied by an in-progress cherry-pick
ORIG_HEAD HEAD before the operation, used by --abort
MERGE_MSG / CHERRY_PICK_MSG pending commit message
kit-conflicts kit sidecar: one line per conflicting path with the conflict kind and base/ours/theirs blob hashes
rebase-apply/ rebase state (head-name, orig-head, onto, todo, actions, done) plus a kit-conflicts sidecar when paused. actions is parallel to todo (pick/reword/squash/fixup, from rebase -i)

Remote / sync:

Agent integration:

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

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

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

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

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

Config / keys / version:

Ignore files

kit reads ignore patterns from both .gitignore and .kitignore at the repository root. .gitignore is applied first and .kitignore last, so under gitignore's last-match-wins rule a repo's own .kitignore can override (including re-include via !) anything .gitignore set. Matching is honored by add, clean, ls-files --others, restore/checkout, and the worktree tree-builder.

Matching is path-relative and follows the gitignore semantics for this v1 subset:

Deferred (documented non-goals for now): nested per-directory ignore files (only the repo root is read), global excludes (core.excludesFile), and escaped trailing spaces.

Divergences from Git

kit's local commands intentionally diverge from Git in a few places. These are documented behaviors, not bugs:

Commands deliberately not implemented (by design)

Several Git commands that were once "wontfix-by-design" have been reinstated under safety guards as part of the git-parity porcelain-completion work: kit revert, kit mv, kit clean, and kit reset --hard are all available and documented above. Each keeps a kit data-loss guard — for example, clean and reset --hard refuse to delete or discard without an explicit -f — rather than adopting git's silent destructive default. Rename detection in diff/log output remains a separate, unscheduled follow-up: kit mv stages the move but status shows it as a delete plus add, not R.

Note: object garbage collection (kit gc) is shipped. History-rewriting commands (commit --amend, reset, rebase) record the superseded commit in the recovery log, and kit gc reclaims unreachable objects once they fall outside the grace window. See docs/specs/SPEC-GC.md.

Config keys

Stored in .kit/config as key = value lines. Kit refuses to load a config file that has an unknown key, a line without =, or a malformed value. The error names the file and the line.

Security-sensitive keys are user-scoped only. Kit warns and ignores them if a repo's .kit/config sets them (a hostile repo must not be able to redirect signing or trust). Those keys — user.identity, signing_key, signer, key.*, attest.*, ssh.*, trusted_remote_endpoint, admission_helper, grant.webauthn_rp, and remote.<name>.admission_headers — live in the user config ($XDG_CONFIG_HOME/kit/config); set them with kit config <key> <value>, which routes them to the user scope automatically.

Key Value Default Notes
user.identity hex Identity unset User-scoped only; see below
signing_key path .kit/keys/default.key Ed25519 seed file; user-scoped only
default_branch name main Branch for kit init
remote_endpoint URL / path empty Set via kit remote add
remote_bucket name empty For s3 remotes
remote_type file / http / s3 / ssh / memory auto
transport_auth bearer / envelope bearer Write-auth mode for kit+https:///kit+http://; envelope additionally Ed25519-signs writes with the commit-signing key (see signer/signing_key/key.ed25519_ref)
http.sslCAInfo PEM certificate file path unset User-scoped only. Additional trust certificates for native Connect HTTPS remotes only. KIT_SSL_CA_FILE takes precedence. See HTTPS certificate trust.
admission_helper absolute executable path unset User-scoped; invoked once for a 402 on a trusted remote. Its stdin includes untrusted server challenge content. The helper decides whether and how much to spend.
remote.<name>.admission_headers comma-separated HTTP header names empty User-scoped additions to the admission helper's request allowlist; hard-reserved names are refused.
ssh.strict_host_key_checking yes / no / accept-new inherit User-scoped only
ssh.user_known_hosts_file path inherit User-scoped only
ssh.identity_file path inherit User-scoped only
signer file / keystore file User-scoped commit signing source
key.backend backend name software User-scoped default for kit key
key.default_ref <backend>:<label> software:default User-scoped fallback key ref
key.ed25519_ref <backend>:<label> software:default User-scoped Ed25519 ref
key.secp256k1_ref <backend>:<label> software:default-secp256k1 User-scoped secp256k1 ref
key.p256_ref <backend>:<label> software:default-p256 User-scoped P-256 ref
attest.signer repo-key / keystore / external repo-key User-scoped attestation signer
grant.webauthn_rp <rp_id> <origin>... (repeatable; | separates entries in one value) unset User-scoped only. Pins the WebAuthn relying parties kit grant/epoch/visibility accept for webauthn-p256 signatures; unset refuses them

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

HTTPS certificate trust

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

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

http.sslCAInfo is user-scoped only. A repository file and a -c override cannot set it, because a repository must not add trust roots. KIT_SSL_CA_FILE overrides the configured value. Configured relative paths start at the repository root; ~/ expands against HOME. An environment file path is used as supplied. The selected file adds certificates to the bundled Mozilla roots; certificate-chain and hostname verification remain enabled. A missing, unreadable, empty, or malformed selected file is a hard error. Keep private keys out of the CA file; it contains certificates only.

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

Admission helper

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

user.identity

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

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

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

ssh.*

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

URL schemes

kit remote add <url> accepts the strict kit+<scheme>://... form only. Anything else is hard-rejected:

error: invalid remote URL '<input>': must start with 'kit+<scheme>://'
hint: URL must start with kit+<scheme>:// (e.g. kit+https://, kit+ssh://, kit+file://, kit+s3://)

Accepted schemes:

Scheme Form Use
kit+file kit+file:///abs/path local filesystem mirror
kit+https kit+https://host[:port][/path] ConnectRPC gateway (for example, VCS Worker)
kit+s3 kit+s3://endpoint/bucket[/prefix] S3-compatible object store
kit+ssh kit+ssh://user@host[:port]/path SSH with the kit shell
kit+memory kit+memory:// in-memory (testing only)

kit+https (and loopback-only kit+http) speaks kit.transport.v1.TransportService over ConnectRPC — see docs/specs/SPEC-TRANSPORT-CONNECT.md. Any /path on the URL is accepted (for shape-consistency with the other schemes) but currently has no wire effect: every RPC resolves to the fixed /kit.transport.v1.TransportService/<Method> path regardless. Write RPCs against a server that requires the Ed25519 write envelope need kit config transport_auth envelope — see the config table above and SPEC-TRANSPORT-CONNECT §7.3.

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

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

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

Version output contract

kit version prints exactly:

kit <X.Y.Z>\n

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

*nix conventions

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

Exit codes

Based on BSD sysexits(3):

Code Constant Meaning
0 ok Success
1 general_error Catch-all for other failures
64 usage Wrong args or unknown subcommand
65 dataerr Malformed input (corrupt object, bad hash)
66 noinput Missing / unreadable input file
69 unavailable Transport could not connect
70 software Internal consistency error (a bug, not bad input)
73 cantcreat Cannot create output file
75 tempfail Temporary failure; retry is safe
76 protocol_error Bad URL scheme or malformed server response
77 noperm Permission denied
78 config_error Unknown config key or invalid value

The constants live in rust/crates/kit-bin/src/exit.rs. Shell scripts can distinguish user typos (64) from transient failures (75) without parsing stderr.

Environment variables

File layout

Path Purpose
.kit/ Repo-local state (like .git/)
.kit/config Repo-local config — overrides user-level values
.kit/keys/default.key Repo-local Ed25519 signing key
.kit/index Staging index
.kit/index.lock Held by commit/checkout/merge/rebase
.kit/COMMIT_EDITMSG Scratch file for kit commit without -m
$XDG_CONFIG_HOME/kit/config User-level config (cross-repo defaults)
$XDG_CONFIG_HOME/kit/grants/ Your grant store: <grant id>.grant files (0700/0600), never repo-scoped
$XDG_DATA_HOME/kit/keys/ User-level keystore (optional)
$XDG_CACHE_HOME/kit/ User-level cache
$XDG_STATE_HOME/kit/ User-level state

Repo-local values in .kit/config win over user-level config except for security-sensitive signer, keystore, external-signer, and identity selector keys that kit intentionally accepts only from user scope or explicit flags.

Signals

Man page

# System install (requires root):
sudo cp man/kit.1 /usr/local/share/man/man1/

# User install (preferred if ~/.local/share/man is on MANPATH):
mkdir -p ~/.local/share/man/man1
cp man/kit.1 ~/.local/share/man/man1/

man kit

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

Shell completions

# bash (macOS / Homebrew)
cp completions/kit.bash /usr/local/etc/bash_completion.d/

# bash (most Linux)
sudo cp completions/kit.bash /etc/bash_completion.d/

# zsh (anywhere on $fpath; example for Homebrew)
cp completions/kit.zsh /usr/local/share/zsh/site-functions/_kit

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