kit 6.6.4
Added
-
kit checkaudits npm's install-script grants. npm no longer runs a dependency's
preinstall/install/postinstall/prepareunlesspackage.json'sallowScriptsnames
it, which makes that field the standing record of which dependencies were handed
install-time code execution. The newinstall-script grantscheck (CWE-829) reads it and
flags the two shapes that outlive their review: a bare-name or range key, which also
covers the release nobody has seen yet, and a stale grant for a package the manifest no
longer depends on — a standing permission for a name an attacker can re-introduce
(npm install-scripts pruneclears those). A field shape npm does not write is reported as
didNotRunrather than read as "no grants", because treating an unparseable allowlist as
empty would report the most dangerous state as the safest one. The shapes were measured
against npm 11.19.0, not taken from the docs page, which describes{pkg: "1.2.3"}while
the client actually writes the version into the key:{"esbuild@0.28.1": true}pinned,
{"esbuild": true}unpinned,{"esbuild": false}denied. -
docs/RELEASING.md— how a release is cut, what the publish job refuses to publish,
and the migration off a long-lived npm token, including the 13 per-package trusted-publisher
configurations that migration needs and the two constraints (workflow_callbreaks npm's
validation; self-hosted runners are unsupported).
Fixed
-
The publish job's tag-signature gate was fail-open, and
v6.6.3shipped unsigned.
The step readif ! git verify-tag "$TAG" 2>&1 | tee /tmp/tag-verify.log; then—git
exits 1 on an unsigned tag, but a pipeline reports its LAST command, andteealways
exits 0. GitHub's default step shell isbash -e {0}, which does not setpipefail, so
the condition was never true: the gate could not fail, and every GPG control around it
(the pinnedMAINTAINER_KEY_FPR, the single-key keyring check, the ownertrust import) was
intact and useless.v6.6.3was tagged withgit tag -aand published;git tag -v v6.6.3printserror: no signature found. Re-signing it would mean force-pushing a
published tag, which re-triggers the publish workflow against a version already on the
registry, so the tag stands anddocs/VERIFY.mdrecords why. Fixed withset -o pipefail
and a redirect instead of the pipe;src/publish-workflow.test.tsasserts the verdict
cannot come from a pass-through sink, and both assertions were mutation-tested by
reintroducing the pipe. -
Self-audit
R1-fail-open-cinow catches the class, not just|| true. A condition
piped into a pass-through sink (tee,cat) takes its status from the sink, so the rule
flags it unless the step actually setspipefail. "Actually" is load-bearing: the first
implementation walked back looking for the word and was satisfied by the fixed step's own
explanatory comment — documented rather than enforced, the same disease the rule exists to
catch. A pipe whose last command IS the question (if cmd | grep -q x) is left alone.
Changed
-
The install gate holds install-script approval, not just installs.
npm approve-scripts <pkg>andnpm install-scripts approve <pkg>are the moment an already-installed
dependency gains arbitrary code execution — the exact decision the gate exists to hold —
and neither was matched, so an agent in auto-mode could grant it unchecked. Both spellings
now triage the named package like an install target;--all/-aand the bare
interactive form name nothing and fail closed;pnpm approve-buildsis covered the same
way. The read-only faces are deliberately never blocked —install-scripts ls,deny,
prune,--dry-runand--allow-scripts-pendinghand out no permission, and blocking the
review step would push an agent straight to--all. -
The supply-chain check's install-script advice names the review, not the skip. It said
"install withnpm ci --ignore-scriptswhere possible"; skipping is npm's own default now,
so that described what already happens. It now points atnpm install-scripts lsand a
per-package pinned grant, never--all. -
The publish job installs
npm@^11.5.1before anything else. npm's OIDC trusted
publishing — the migration target now that 2FA-bypass tokens lose direct publish rights
(~January 2027) — exists only in npm ≥ 11.5.1 on node ≥ 22.14.0, andactions/setup-node
ships npm 10.9.x for node 22. Verified against this repo on npm 11.19.0 before wiring:
npm ciandnpm publish --dry-runbehave identically, and npm's skip-install-scripts
default costs nothing here (esbuild's postinstall is the only one in the tree; build and
tests pass without it).src/publish-workflow.test.tsasserts the step exists, clears the
version floor, and precedes every publish — a comment cannot fail CI, and a publish job
runs only on a tag push, the worst moment to learn the client is too old. -
kit initgenerates only what the repo proves — and asks about the rest. The
operator's~/.kit/defaults.toml [init] serviceswere appended to every new project, so a
repo with no PostHog got a[services.posthog]block and three POSTHOG keys, and the
framework table handedverify = "pnpm build"to repos that use npm. A plausible generated
value is worse than an absent one: an absent line is a question, a plausible line looks
like a decision somebody made, so nobody re-reads it.known_servicesis now a menu —
detected services are written, known-but-absent ones come back asofferedand are put to
whoever can answer —generateTomlreturns{ toml, gaps }where every declined field
carries the command that settles it, and the oldserviceskey still reads with a rename
notice.promptMultiSelectreturnsnullrather than a default when nobody is there,
which is why it exists besidepromptSelect, whose "answer with the recommended option"
convention had been silently choosing a secret backend on every agent and CI run. Also:
bun.locknow counts as bun (the 1.2 rename to the text lockfile made every current bun
repo detect as npm), and the build refuses to run on cloud-sync conflict copies instead of
hiding them behind atsconfigexclude that never matched. -
kit_checkover MCP answers the question and offloads the rest. The standing MCP
surface is paid once per session (7,788 chars); akit_checkresponse is paid on every
call, and an agent in a check → fix → check loop calls it repeatedly. The response is now
the verdict plus every non-passing row — 9,799 → 3,210 chars, 67% off per call — with
the complete run written to.kit/runs/check-<stamp>.jsonand returned asdetail.path
(detail: truestill returns the whole document inline). Two invariants bound the saving:
onlypassrows may be omitted, so askipordidNotRunalways survives and omitted
passes stay counted; andscopeis carried verbatim, so a--category-narrowed green
cannot read as a whole-repo green. A failed detail write returns no reference at all — a
verbose answer beats a dangling pointer.scripts/measure-mcp-output.mjsreproduces both
numbers.
Full changelog: https://github.com/sandstream/kit/blob/v6.6.4/CHANGELOG.md
Verify this release:
git tag -v v6.6.4
npm audit signatures