kit 6.8.0
Fixed
-
kit config migratedeleted every comment in.kit.toml, and the dry run did not say so
(#513). It re-serialised from the parsed object, so on the v0 → v1 step — whose entire job is to
stampversion = 1— kit's own config went from 8 comment lines to 0, and a one-line change
produced a 36-line diff. The parsed data was identical, so the backup-and-re-validate safety
path passed and flagged nothing. What was lost was policy reasoning: why those scanners are
declared, why the refs areaqua:-scheme-qualified, which valuesenvironmentaccepts.An add-only migration is now applied as a text edit — the added key is inserted above the
first table (top-level keys must precede it or TOML scopes them into it) and the rest of the
file is untouched. Anything more structural returns null from the patcher rather than being
forced through an edit that cannot express it, and then the run is refused while comments
exist unless--allow-comment-lossis passed, naming how many lines are at stake.--dry-runnow says which path would be taken — "Applied as a text edit: comments and
formatting are preserved" or the count that would be deleted. A preview that omits the
destructive half of a change is worse than no preview.
Added
-
Config drift is reported, both kinds (#511). A
.kit.tomlfalls behind in two ways and
neither reached the operator.Schema drift had a detector and no caller.
kit config migrate --checkhas answered
"Config is at v0; current is v1" since versioning landed, andkit statussaid only
✓ .kit.toml present— "present" is not "current". Measured on kit's own repo: the config was
v0 while the schema was v1, and nothing said so.kit statusnow carries aconfig schemarow
(○ config schema v0, current is v1 → run kit config migrate) via a new read-only
planConfigMigration()that answers without a write path.Feature drift had no detector at all. A long-lived config does not know about the sections
kit has learned since —[context],[policy.agent_writes],[deploy],
[governance.audit] require_anchor.applyRecommendedHardening()knew the recommended posture
but was reachable only fromkit setup, which APPLIES it (touching~/.claude,~/.codexand
the repo's git hooks), so asking "what would I get?" meant letting it happen. New
kit config recommend [--json]reports it and writes nothing.Each row says what the piece buys, not merely that it is absent — a checklist of missing
features is how a report gets ignored. And the pre-push context gate points at declaring
[context]first rather than at installing a gate that cannot fail, matching the refusal added
in #497.Deliberately not a new
kit rebaseverb:rebasemeans something specific and different to
every git user, and both halves of this were a missing reader rather than a missing command.
Auto-migrating on load is also out — a config that changes shape underneath a session is worse
than one that is behind, and the schema version exists so the change is explicit and reviewable.
Fixed
-
The machine-wide wrapper pointed at whichever kit wrote it (#509).
~/.kit/bin/kitis what
every kit hook in every repo execs, andensureKitWrapper()baked inprocess.argv[1]— so one
kit hooks addrun from a development checkout aimed the whole machine's enforcement floor at
…/dist/cli.js, a pathnpm run builddeletes on its first step. Measured: a session in an
unrelated repo failed withkit CLI entrypoint missing: …/kit-public/dist/cli.jsand kept going
ungated, because a hook that cannot start is reported non-blocking.Four properties made it worse than a stale path, and the fix addresses each: a repo-local
command had a machine-wide effect; a dev build is transient by design; it failed open; and it
was silent in both directions.Now: an installed kit wins over a checkout when the wrapper is written (probing the usual
global bin dirs),KIT_WRAPPER_ALLOW_DEV=1still wins over that so an explicit dev pin is not
silently overridden, and a checkout with no install to fall back on is written but loud
— refusing outright would leave the hooks with no wrapper at all. The result line names the
entrypoint instead of just saying "refreshed".kit check'sgit hook floorrow now also judges the wrapper, since the hooks resolve through
it: a missing entrypoint fails high (every hook on the machine is dead, and it fails open),
and an entrypoint inside a git working tree warns (it works until the next build).Both new env knobs are registered in
kit config knobs:KIT_WRAPPER_ALLOW_DEV(marked
dangerous) andKIT_TOOL_LATEST_TTL_H, which had been missing since it was introduced.
Fixed
-
The install-gate recognised one repo-argument form; the tool publishes two (#507).
ownerRepoArgmatched^[\w.-]+/[\w.-]+$— one slash, no scheme — so the payload of a
repo-fetching installer was triaged only when the argument happened to be written as
owner/repo. Measured against 6.7.0, withnpm install left-padstill blocking as a control:npx skills add cursor/plugins → BLOCKED npx skills add https://github.com/cursor/plugins --skill unslop → allowed npx skills add github.com/cursor/plugins → allowed npx skills add git@github.com:cursor/plugins.git → allowedAnd the bypass ran through the form the tool's own help recommends —
skills add --helplists
bothvercel-labs/agent-skillsandhttps://github.com/vercel-labs/agent-skills, so this was
never a latent edge case. The argument is now normalised BEFORE matching (any scheme, an
optionaluser@, a host segment, scp-stylegit@host:owner/repo, a trailing.git, and a
deeper…/tree/main/subpath all reduce toowner/repo) rather than the pattern being
widened, which would have started matching npm scopes and file paths.@scope/nameis still
not a repo, and neither is a filesystem path —./local/path/file.txtused to reduce to
./local, which the table test caught. -
The pip maintainer count was declared unavailable on a claim that was wrong. The previous
entry said PyPI publishes no maintainer list. Measured, it does:requestscarries
maintainer_email = "Ian Stapleton Cordasco <…>, Nate Prewitt <…>". Two things still keep it
from being npm's number, and both are now in the output rather than in a comment: PEP 621
leavesmaintainernull so the value hides inmaintainer_email(and sometimes only in
author_email—opensandbox-serverhad exactly that), and PyPI's list is self-declared
package metadata while npm's is the registry's own record of who may publish. So the probe
runs, reportsN declared maintainer(s) in <field> (self-declared package metadata), and when
it warns it says outright that this is not registry publish rights and not comparable to
npm's maintainer count. A package that names nobody still declares the probe unavailable —
with an accurate reason this time.Consequence, measured:
pip opensandbox-servergoes from100/100with a partial-coverage
note to88/100with a single-maintainer warning — the same reason npm's packages scored 88.
Counting is bracket-aware, so"Cordasco, Ian <one@example>"is one maintainer, not two.
Added
-
[context]can declare the IDENTITY a repo must use, and kit asserts it (#503).kit check
printed✓ vercel authenticated <account>and compared it to nothing, whilekit context check
asserted only ids read from repo-local files — so a CLI answering as the wrong account was
green. New fields:vercel.user,github.user(distinct fromgithub.org, which is the
remote's owner) andconvex.account. They behave exactly likegit.email: declared → asserted,
mismatch → red and non-zero, unreadable →unknownrather than a mismatch, since "cannot tell"
and "wrong account" are different findings.What the gap cost, measured: a session whose CLI was logged in as a personal account with
read-only rights on the production environment read a FILTERED variable list as a complete one.
env lsshowed four variables and looked whole (the ones that also existed in preview), the
production-only ones were invisible, andenv pull --environment=productionfailed as though
the variable did not exist. Two contradictory conclusions were drawn before a web UI settled it.A mismatch now also names the mechanism that scopes an identity to a repo, because a global
login is what produces a wrong one:vercel -Q <dir>/VERCEL_TOKEN,gh auth switch,
gcloud config configurations activate,AWS_PROFILE, stripe project profiles — and for
convex, that it has no profiles (~/.convex/config.jsonis global andconvex login
overwrites it), so the isolation isCONVEX_DEPLOY_KEY/CONVEX_DEPLOYMENTper repo.And it says the part that turns a wrong identity into wrong conclusions out loud: anything you
already read as this identity may have been a PARTIAL view — permissions filter listings
silently.kit context check's ready-to-paste block offers the detected identities, hedged (⚠ the gh account logged in NOW — VERIFY it is right for THIS repo), since the live identity is exactly
what the lock exists to question. A value kit could not resolve to a real identity is reported
but never offered as something to declare.
Added
-
kit tools list— the inventory that did not exist, and the measurement three surfaces
were missing (#500). Per tool: the resolved path, the installer that owns it (classified from
the path, never assumed), the installed version, and with--latesthow far behind it is.
Covers the declared[tools]and the undeclared CLIs an agent decides from —gh,op,
jq,mise,docker,gcloud,kubectl,supabase,stripe,psql, … — which were
invisible precisely because nothing declared them.Measured on the machine that filed the report, first run:
trivy 0.72.0 → 0.74.0,
trufflehog 3.95.9 → 3.97.0,gh 2.96.0 → 2.97.0,mise 2026.6.11 → 2026.8.9,
npm 10.9.8 → 12.0.2,op 2.34.0 → 2.39.0. Cold 12s, warm 4s. -
latestis now checked against the installer that would satisfy it.versionSatisfies
answeredif (required === "latest") return true;, so✓ vercel 53.1.1 (need latest)printed
while the registry had 59.1.4 — six majors. The tools table now shows the installer in brackets
and, for alatestpin,!with53.1.1 → 59.1.4 available. A pin ofany/present/*
asserts presence only, and says so, for repos that genuinely mean "whatever is installed".Lookups are per-installer (
npm view,mise latest,brew info --json=v2,pip index),
cached per machine in~/.kit/tool-latest.jsonwith a TTL (KIT_TOOL_LATEST_TTL_H, default
24h), so the gate makes no network call once warm. A failed lookup is not cached — a flaky
network must not become a day-long policy.
Fixed
-
cli-lock.jsonrecorded a provenance it never measured, and the check called itin sync.
Both writers did the same thing:tools[name] = { version, source: "mise" }; // version = the DECLARED string
so an entry read
{"vercel":{"version":"latest","source":"mise"}}for a binary in
/opt/homebrew/binthat mise does not manage, and the lock check compared names only.
Now the version is the resolved one, the source is classified from the path,sourceDetail
carries installers the lock's four-value vocabulary cannot name (brew,system,cargo,
kit-shim), and a recorded source that contradicts the PATH winner fails the row with
provenance drift: vercel: lock says mise, but /opt/homebrew/bin/vercel comes from brew. -
An unrunnable currency check now reports why, instead of passing. Air-gap makes no outbound
call and says so; an installer kit cannot query (system,cargo,go, a kit PATH shim) says
which; a timeout or an unparseable answer says that. Same rule as the scanners: coverage that
could not run is UNKNOWN, never clean. -
Prefixed declarations were reported as not installed.
[tools]carries backend prefixes
(aqua:aquasecurity/trivy,npm:@socketsecurity/cli), and the probe used the raw declaration
rather than the executable name — the same false statement as the one above, pointed the other
way.
Fixed
-
The install-gate's refusal described its own machinery instead of the hazard it caught.
A blockedgit commit -m "… \deployment:env:view` …"` was reported as a false positive; the
gate was right. Backticks inside DOUBLE quotes are command substitution, so the shell runs the
token and splices its output into the argument — measured, the words silently vanish:$ bash -c 'echo "... the permission `deployment:env:view` ..."' bash: deployment:env:view: command not found ... the permission ...The refusal named a triage target and offered
kit triage/kit pkg, both nonsense for that
command, so the operator concluded kit had mis-parsed a commit message and routed around it with
-F— hiding a command that would have committed a message with a hole in it. The refusal now
names the substitution, says single quotes are literal, and points atgit commit -F <file>;
the triage advice is appended only when the block really is about an install target. Fail-closed
behaviour is unchanged (#501).
Full changelog: https://github.com/sandstream/kit/blob/v6.8.0/CHANGELOG.md
Verify this release:
git tag -v v6.8.0
npm audit signatures