kit 3.0.0
kit 3.0 — from a provable local floor to an org-governed control plane: machine
identity (Ed25519) + signable, distributable policy-as-code, governed
cross-device memory (encrypted sync + device-coupled action items),
attribution-bound tamper-evident audit, a hardened install/scan gate, and a
deterministic, zero-LLM "smart" UX (contextual hints + a discoverable knobs
reference). Everything below is additive over 2.x — no stable command was
removed or downgraded (enforced by the public-surface invariant).
Security
- Audit attribution is now bound into the HMAC anchor (de-attribution of a
sealed entry is caught). The adversarial pass found that a writer-only
attacker — no private key, no anchor key — could strip or rewrite the
kid/sigof an entry that was already signed AND anchored, and every layer
stayed green: the keyless chain re-hashes without those fields, the signature
tally just re-counts the entry asunsigned(fail-open), and the hash-only
anchor tip was byte-identical. The anchor seal now folds attribution
(computeAnchorTipV3overhash | kid | sigper line; anchor record
version: 3), so altering or removing the signer of any anchored entry
changes the tip → hardtip-mismatch; forging akid/sigonto a sealed
keyless entry is caught the same way. Backward-compatible: legacyversion ≤ 2
records keep verifying hash-only until the nextkit audit anchorre-seals them
as v3. (Surfaced by the deep adversarial security pass; complements #175.)
Added
kit memory installnow wires the Claude Code status line too — the setup
score + open-PAL (⚠) count become visible in your terminal. Previously PAL
only reached the agent (via the hook); the human never saw it unless they
hand-editedsettings.json. Install now also setsstatusLinetokit statusline— idempotent, and it never clobbers a custom statusLine (reports
it and leaves it as-is).--no-statuslineskips it;kit memory uninstall
removes only kit's own. (Paired with the device-coupled PAL count, so what you
see in the bar is this device's real "blocked-on-you".)- PAL is device-coupled — an ephemeral session's action items no longer nag your
durable device. Pending-action items are now stamped at creation with an
origin_device(stable per machine; an ephemeral container gets its own
throwaway id) and anorigin_root(the absolute project path). Reminders, the
kit statuslinecount, andkit memory pal listsurface only this device's
items by default (legacy NULL-origin rows still show) — so a "blocked-on-you"
created in a throwaway container/scratch dir doesn't follow you across the gap-#4
memory sync.kit memory pal list --allshows every device;kit memory pal prunecloses this device's open items whose origin directory no longer exists
(dead ephemeral/scratch dirs). Schema v5; backward-compatible (pre-v5 rows have
no origin and are left untouched by prune). kit config knobs— a discoverable reference for the power-user env vars +
.kit.tomlfields kit honors. A review found capabilities like air-gapped
SAST (KIT_SEMGREP_CONFIG), capture-time secret redaction (KIT_MEMORY_REDACT),
read-only lockdown ([policy].default_mode), monorepo triage whitelisting
([supply_chain].internal_scopes) and the CI escape hatches (KIT_ELEVATED,
KIT_PROD_OK,KIT_NON_INTERACTIVE) were only findable in source.kit config knobslists them grouped, withenv/cfgtags and a ⚠ on the ones that bypass
a safety gate (--jsonfor tooling). Listed inkit confighelp + the main
command list. (Companion to the deterministic hint engine.)- Smart, deterministic hints — kit surfaces the right opt-in capability at the
right moment (zero LLM). A review found many powerful features (signed policy,
audit anchoring, container/IaC scanning, malware heuristics) were only
discoverable by reading source. A tiny rule engine (src/hints.ts) emits one
short, actionable tip from plain state checks — e.g. "your audit log isn't
anchored — runkit audit anchor", "your.kit-policy.tomlis unsigned — run
kit policy sign", "you have a Dockerfile but trivy isn't installed",
"malware heuristics are off — enable GuardDog", "you have an identity but no
org policy". Shown as a💡 tip:line afterkit checkand at session start.
Each tip shows at most once (a~/.kit/.hint-*marker suppresses it),
detectors are fail-soft (a tip never breaks a check or a session), and
KIT_NO_HINTS=1silences them all. - Private cross-device memory sync over your own git remote (
kit memory push
/kit memory pull). Memory design gap #4: the personal store
(~/.kit/memory.db) is per-machine; this wires the existing encrypted-backup +
mergeDbprimitives to an opt-in transport — YOUR private git repo — so one
machinepushes and anotherpulls (last-write-wins), no manual file copy.
Configurable without being a backdoor, by construction: (1) config is read ONLY
from~/.kit/sync.toml(a LOCAL file), never the project tree, so a malicious
committed.kit.toml/.kit/*in a cloned repo can't redirect your memory; (2)
the sync remote MUST differ from the project'sorigin(anti-exfil guard) — your
secret-dense brain can't be pushed into the project repo; (3) the payload is
AES-256-GCM encrypted (the remote sees only ciphertext; passphrase via
KIT_MEMORY_PASSPHRASE, never stored); (4) fully opt-in — no config, no sync.
Zero new dependencies (git +node:crypto). - Memory sync: a
transport = "command"option for non-git stores
(S3/rclone/scp/USB). You're not locked into a git remote — set
transport = "command"in~/.kit/sync.tomlwithpush_cmd/pull_cmd, and kit
runs your command with the encrypted blob path exposed as$KIT_MEMORY_BLOB
(e.g.aws s3 cp "$KIT_MEMORY_BLOB" s3://…,scp,rclone). Same guards
apply: the command is read only from the LOCAL~/.kit/sync.toml(never the
project tree, so a cloned repo can't inject it), the payload is encrypted, and
it's opt-in. The blob is the real unit; git and command are just two ways to
move it. - Memory sync onboarding:
kit memory sync init+ opt-in auto-pull/push + a
one-time nudge. Closes the usability gaps in gap #4 (you previously had to
hand-write~/.kit/sync.toml, and nothing synced automatically):kit memory sync initwrites the local~/.kit/sync.tomltemplate
(--remote <url>for git, or--commandwith--push-cmd/--pull-cmd;
--autoenables the hooks;--forceoverwrites). It won't clobber an existing
config and reminds you to setKIT_MEMORY_PASSPHRASE+ create the private repo.[memory.sync] pull_on_start = true/push_on_end = truewire sync into the
SessionStart/SessionEnd hooks: pull+merge before "where you left off", and
index+push when the session ends — the missing piece for ephemeral
containers (memory reaches your durable store before the box is reclaimed).
Both are fail-soft: a session is never blocked by sync.- A one-time tip suggests
kit memory sync initwhen you have a non-trivial
memory store but sync isn't configured yet (suppressed after the first show).
Security
- Install-gate: closed four bypasses that let an untriaged install through. A
deep adversarial pass found thePreToolUseinstall-gate's command parser could
be defeated by (1) a leading env-var assignment —A=1 npm i eviltokenized so
no matcher fired; (2) package runners it didn't know —npm exec/pnpm dlx/
yarn dlx/bun x; (3) a remote tarball URL —npm install https://…/x.tgz
was dropped as a "local" target on its extension alone; and (4) an install hidden
in a subshell or-carg —sh -c '…',$(…), backticks. The parser now strips
leading wrapper bins +VAR=valueassignments, matches the runners, treats any
scheme://target as remote (→ fail-closedunverifiable), and recursively scans
nested commands. A regression test pins each bypass. - ReDoS: the GCP private-key redaction pattern no longer hangs on a small hostile
input."private_key"was matched with two unbounded runs around a literal, so
a blob of near-miss-----ENDtokens backtracked catastrophically (~18 KB hung
~17 s) — a CPU DoS of the gate reachable viascan-staged,kit memory scan, and
status redaction. The body is now a single bounded[^"]{1,8000}run ending at the
close quote (linear; still matches real keys). - ReDoS:
globToRegExp(memory clusters) collapses stacked wildcards. A
malicious committed.kit/shared/clusters.jsonglob of many**/**/compiled to
adjacent.*/(?:.*/)?groups that backtracked catastrophically — and the cluster
map is matched on everyUserPromptSubmit, so a pulled branch could hang the agent
on each prompt. Consecutive wildcard runs now coalesce to a single non-stacking
group and an over-long glob is refused (matches nothing). - Policy trust anchor: canonical signing bytes now cover every key faithfully.
sortDeeprebuilt objects without[k] = …, so a__proto__key was a prototype
write that silently dropped the key+subtree — letting a[__proto__]table be
appended to a SIGNED.kit-policy.tomlwithout changing its signature/fingerprint
("signed bytes ≠ document"). It now assigns viadefineProperty(the key is a real
own property in the bytes), refuses a TOML date value (no faithful canonical
form → a Date≡string collision),validatePolicyrejects__proto__/constructor/
prototypekeys,verifyPolicytreats an uncanonicalizable doc asinvalidinstead
of crashing, andversionGteis guarded against a non-string. No global prototype
pollution existed (NodeJSON.parse/smol-tomlcreate an own property); this hardens
canonicalization soundness.
Fixed
- Air-gap completeness: the update check no longer egresses by default under
KIT_AIRGAP.checkForUpdate(the one outbound call on a normalkitrun)
now short-circuits when air-gap is set, alongside the existing CI/KIT_NO_UPDATE_CHECK
suppressors — so "no outbound network by default / air-gap mode" is a complete
posture, not one with a lone npm-registry beacon. (Surfaced by the 3.0 promise audit.) - Deterministic gate: bumblebee catalog-staleness no longer flips the verdict on
wall-clock time. A clean supply-chain scan whose threat-intel catalogs are >60
days old was awarn(failingkit ci --strict) purely because the calendar
advanced — same repo, same scanners, different verdict by date. Staleness is now
ADVISORY (status: pass, noted in the detail/suggestion); the gate verdict is a
function of inputs only. (Surfaced by the 3.0 promise audit.) - Secrets: entropy backstop closes a fail-closed shared-memory scan hole. The
kv-secretheuristic allowlists runtime env prefixes (KIT_/GITHUB_/…), so a
real high-entropy credential stored under such a prefix slipped past the
fail-closedkit memory sharegate.findSecrets(text, { entropyBackstop })now
also flags an ALL-CAPSKEY=valuewhose value is long + genuinely high-entropy
(Shannon ≥ 4.2 bits/char) regardless of prefix;shareEntryenables it. Catches
base64/base62 secrets while clearing hex hashes (~4.0) and dictionary values; the
noisier code/diff scan is unchanged. (Surfaced by the 3.0 promise audit.)
Changed
- Frozen contracts: "additive-only" is now test-enforced, not just review
discipline. A new invariant test checks the live surface against the committed
contracts/public-surface.jsonbaseline and FAILS on a removed stable command, a
stable → experimental/deprecateddowngrade, or aschemaVersion/adapter-sdk
major regression — closing the gap where a breaking change could be hidden by
regenerating the byte-for-byte snapshot. (Surfaced by the 3.0 promise audit.)
Added
-
Org-distributed policy verification (
kit policy trust) — 3.0 Phase 2
starter. A committed.kit-policy.signerstrust anchor lists the org public
key(s) allowed to sign the policy;verifyPolicyresolves the signer in trust
order (a pinned--key→ this machine's identity → the org anchor), so ONE
policy signed by a central org key verifies authentically on every clone — no
shared secret, only public keys distributed.kit policy trust <pubkey.pem> [--label]/--list/--remove <id>manage it. Fail-CLOSED once an anchor
exists: a signer that isn't in it failskit policy check/kit ci(same
discipline as the HMAC audit anchor), not just a warn. Newsrc/policy-trust.ts;
PolicyVerifyResultgainsvia(key|local|org) +anchored. This is the model
the major bump needs — identity signs the org standard, any repo verifies it. -
kit cienforces the signed policy.kit cinow foldsevaluatePolicy
into its gate: a present.kit-policy.toml's requirements appear aspolicy/*
checks in the report (text/GitHub/JSON), so a tampered/revoked signature, unmet
min_kit_version, or (under--strict) a missing required scanner fails CI like
any other check. Opt-in and backward-compatible — no policy ⇒ nopolicy/*
checks ⇒ the verdict is unchanged. Completes Phase 1's "kit ci consumes the
policy". -
kit policy check(experimental) — enforce the signed.kit-policy.toml
against this machine's deterministic state (3.0 Phase 1, part 2). Verifies the
signature (the trust anchor: warn on unsigned/unknown signer, fail on
tamper or a revoked signer), then evaluatesmin_kit_version(current ≥
required),required_scanners(resolvable mise-first; warn, or fail under
--strict), andprod_writes_need_approval(.kit.toml [governance.approval]).
require_triage/thresholdsare surfaced (enforced by the install-gate /
data-source plugins, not duplicated). Opt-in: no policy ⇒ no-op.--json+
exit code for CI. NewevaluatePolicy/versionGteand a sharedverifyPolicy
core (reused bykit policy verify). Seedocs/POLICY.md. -
kit policy(experimental) — signable, distributable policy-as-code (3.0
Phase 1). A separate.kit-policy.tomldocument holds the org standard
(require_triage,required_scanners,prod_writes_need_approval,
min_kit_version,[thresholds]);kit policy init/show/validate/sign/verify.
Signing ties the standard to akit identity(Phase 0) over canonical JSON
(key-sorted), so the signature survives TOML reformatting/comments and breaks
only on a real policy change;verifychecks against locally-known keys (or a
pinned--key), fail-opens on an unknown signer, and fails on a tampered doc or
a revoked signer. Separate from the 2.x.kit.toml [policy.agent_writes]
pre-approval — this is the org-level standard. Enforcement glue (kit check/ci
consuming it) + signed org bundles + RBAC are the follow-ups. Seedocs/POLICY.md. -
Zero-touch environment fueling guide (
docs/ENV_FUELING.md). Documents the
setup-script pattern —kit setup --recommended(config: tools, vault-backed
secrets, agent gates, verify) +kit memory sync/restore(recall) — so a fresh
or ephemeral environment (cloud container, Claude Code on the web, CI, new
laptop) fuels itself with no manual steps. Covers non-interactive setup, where
the script lives (devcontainer/Dockerfile/CI/web), the memory bridge, and the
guardrails (secrets never plaintext, private memory never committed, idempotent,
zero-LLM). Linked from the README quick start. -
Path→cluster push-surfacing for shared decisions (
kit memory context).
Pull recall can't guarantee you see a settled decision (a bad query misses
it); the guardrail is now PUSH — touch files under an area and kit
deterministically surfaces that area's ACTIVE decisions. A committed
.kit/shared/clusters.jsonmapsarea → globs(zero-dep glob→regex matcher).
kit memory context [paths…|--changed]prints the active decisions for the
touched areas, and the UserPromptSubmit reminder now appends a bounded notice
for the area(s) your working-tree changes fall into. Superseded/reversed
decisions never resurface (usesactiveShared). Fail-open (no map ⇒ nothing). -
kit panic(experimental) — one-command compromise response (the control
plane's kill-switch). Rotates the local identity (old key archived but its past
signatures stay verifiable), emits a SIGNED, append-only revocation of the old
key (~/.kit/revocations.jsonl, signed by the new key — asymmetric, so it
propagates as public data with no shared secret), records anidentity.panic
event in the tamper-evident audit log, and prints the platform-revocation
checklist for the accounts kit does NOT own (GitHub / Anthropic / Apple / vault)
with links.kit audit verifynow surfaces entries signed by a revoked key
(valid as history; the key is no longer trusted for new signatures). New
identity primitives:recordRevocation,loadRevocations,isRevoked,
revocationStatement. Honest boundary documented: kit owns its keys + the
revocation list + the audit; it only orchestrates platform-account revocation. -
Living shared decisions — lifecycle (status / supersedes / reverses).
Shared memory entries are now versioned: a change is a NEW append-only entry
that--supersedes <id>or--reverses <id>an old one (or carries an explicit
--status).kit memory sharevalidates the referenced id exists. Surfacing
shows only ACTIVE decisions with their age (e.g.2y ago) so an aging decision
is flagged for review, never blind obedience;kit memory area/searchbadge
superseded/reversed entries (and show the chain) so "this was tried + reversed"
still surfaces.activestays implicit (absent field) so pre-lifecycle entries
are byte-identical. NeweffectiveStatus,activeShared,formatAge. -
Shared (curated) memory folded into recall.
kit memory searchnow also
searches the committed.kit/shared/memory.jsonltier and surfaces matching
team decisions/conventions above raw transcript hits (--jsonnow returns
{ messages, shared }). SessionStart recovery re-injects the project's most
recent durable shared decisions (kinds: decision/convention/security/status),
so a resumed session regains the settled context, not just the last few raw
turns. Both paths are project-local and fail-open (a missing/broken shared
store never breaks recall). -
Berget AI + Grunden.ai as known providers. Added
bergetandgrunden
(EU-sovereign, OpenAI-compatible inference) to the service registry with their
canonical key names (BERGET_API_KEY/BERGET_BASE_URL,
GRUNDEN_API_KEY/GRUNDEN_BASE_URL) and where to get them, sokit secrets
treats them as known, vault-resolved keys (never plaintext). Catalog-only by
design: both are used via theopenaiSDK with a custom base URL, so there is
no unique package signal to auto-detect on (keying on theopenaidep would
misattribute every OpenAI user). Agent-backend wiring + a Berget Code gate
adapter are deferred. -
Identity-signed audit entries. When a local
kit identityexists, each
appended.kit-audit.jsonlline is signed: kit attacheskid(signer id) +
sig(Ed25519 signature over the line hash). Best-effort and append-safe — no
identity means entries are written as before, and a signing failure never
blocks the append.kid/sigsit outside the hashed remainder, so the hash
chain still verifies across mixed signed/unsigned/legacy logs.kit audit verifynow reports the signature tally (verified / unknown-key / unsigned)
against the locally-known keys (current + rotated) and FAILS hard on a forged
signature, while fail-opening on absent/unknown trust. This is the asymmetric
ATTRIBUTION layer (who produced an entry, verifiable with only the public key)
orthogonal to the symmetric HMAC INTEGRITY anchor. Seedocs/AUDIT_ATTESTATION.md§2.5.
Fixed
kit checksecrets scan: degraded (no-trufflehog) fallback no longer cries
high-severity on its own noise. The basicgit greppath now mirrors the
trufflehog branch's philosophy — it's UNVERIFIED, so it's amediumwarn, not
high, and it filters its two dominant false-positive classes: test/fixture/mock
files (fake credentials live there by design; the authoritative trufflehog + CI
gitleaks scanners still cover them and verify live) and all-caps identifier
VALUES (an env-var name likeSOCKET_SECURITY_API_TOKENis a config key, never a
secret). Removes a self-inflicted false positive on kit's own source and the
test fixtures; the authoritative scanners are unchanged.
Full changelog: https://github.com/sandstream/kit/blob/v3.0.0/CHANGELOG.md
Verify this release:
git tag -v v3.0.0
npm audit signatures