Skip to content

kit 6.8.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 11:51
· 32 commits to main since this release
5540d19

Fixed

  • kit config migrate deleted 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
    stamp version = 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 are aqua:-scheme-qualified, which values environment accepts.

    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-loss is passed, naming how many lines are at stake.

    --dry-run now 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.toml falls behind in two ways and
    neither reached the operator.

    Schema drift had a detector and no caller. kit config migrate --check has answered
    "Config is at v0; current is v1" since versioning landed, and kit status said 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 status now carries a config schema row
    (○ 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 from kit setup, which APPLIES it (touching ~/.claude, ~/.codex and
    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 rebase verb: rebase means 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/kit is what
    every kit hook in every repo execs, and ensureKitWrapper() baked in process.argv[1] — so one
    kit hooks add run from a development checkout aimed the whole machine's enforcement floor at
    …/dist/cli.js, a path npm run build deletes on its first step. Measured: a session in an
    unrelated repo failed with kit CLI entrypoint missing: …/kit-public/dist/cli.js and 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=1 still 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's git hook floor row 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) and KIT_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).
    ownerRepoArg matched ^[\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, with npm install left-pad still 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                → allowed
    

    And the bypass ran through the form the tool's own help recommends — skills add --help lists
    both vercel-labs/agent-skills and https://github.com/vercel-labs/agent-skills, so this was
    never a latent edge case. The argument is now normalised BEFORE matching (any scheme, an
    optional user@, a host segment, scp-style git@host:owner/repo, a trailing .git, and a
    deeper …/tree/main/sub path all reduce to owner/repo) rather than the pattern being
    widened, which would have started matching npm scopes and file paths. @scope/name is still
    not a repo, and neither is a filesystem path — ./local/path/file.txt used 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: requests carries
    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
    leaves maintainer null so the value hides in maintainer_email (and sometimes only in
    author_emailopensandbox-server had 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, reports N 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-server goes from 100/100 with a partial-coverage
    note to 88/100 with 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, while kit 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 from github.org, which is the
    remote's owner) and convex.account. They behave exactly like git.email: declared → asserted,
    mismatch → red and non-zero, unreadable → unknown rather 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 ls showed four variables and looked whole (the ones that also existed in preview), the
    production-only ones were invisible, and env pull --environment=production failed 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.json is global and convex login
    overwrites it), so the isolation is CONVEX_DEPLOY_KEY / CONVEX_DEPLOYMENT per 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 --latest how 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.

  • latest is now checked against the installer that would satisfy it. versionSatisfies
    answered if (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 a latest pin, ! with 53.1.1 → 59.1.4 available. A pin of any / 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.json with 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.json recorded a provenance it never measured, and the check called it in 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/bin that 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 blocked git 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 at git 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