fix(v0.15.1): RED-1 CVE + typed-handoff + schema minItems
Security hotfix — v0.15.1 Wave 1 fixes from 4-parallel audit. RED-1 (CVE): KEI_DISABLED_HOOKS tokenized match — was `*all*` substring-glob (trivially bypassable via "install", "wall-clock", etc.), now exact-token split on comma/space. Patched in all 9 hooks: no-hand-edit-agents, assemble-agents, assemble-validate, tomd-preread, agent-fork-logger, site-wysiwyd-check, error-spike-detector, milestone-commit-hook, session-end-dump. RED-2 (observability): minimal profile whitelist now includes agent-fork-logger and session-end-dump (ledger + trace paths) so observability is not silently lost on minimal installs. HIGH: review.json schema minItems:1 on findings — rejects empty reviews; new Rust test review_schema_rejects_empty_findings. HIGH: typed-handoff wire-up — produces_artifact declared at top level on 5 manifests (kei-security-auditor, kei-validator, kei-architect, kei-code-implementer, kei-critic); duplicate per-handoff declarations removed. MED: kei-artifact validate.rs gains warn_unsupported_keywords — non-fatal stderr warning when schema uses keywords outside the hand-rolled 2020-12 subset. LOW: CI Node matrix dropped 18, now ['20','22']. Doc drift: skills/hooks-control/SKILL.md reflects tokenized-match semantics and updated minimal-profile hook list. Tests: 191 Rust workspace + 30 assembler (both pass). RED-1 reproducer 10/10 (4 former-CVE vectors blocked, 5 legit vectors accepted, empty passes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b62b219500
commit
f77c1b7fdc
19 changed files with 193 additions and 50 deletions
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
node: ['18', '20', '22']
|
||||
node: ['20', '22']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
|
|
@ -73,4 +73,7 @@ jobs:
|
|||
- run: sudo apt-get update && sudo apt-get install -y shellcheck
|
||||
- name: shellcheck (advisory)
|
||||
run: find hooks _primitives -name '*.sh' -exec shellcheck -S warning {} +
|
||||
continue-on-error: true # warnings are advisory initially
|
||||
# v0.15.1: kept advisory because local shellcheck sweep not yet clean
|
||||
# (quoted-var nits in hooks). Flip to fatal once the sweep is committed;
|
||||
# planned for v0.16.
|
||||
continue-on-error: true
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ produces_artifact = "spec"
|
|||
[[handoff]]
|
||||
target = "kei-code-implementer"
|
||||
trigger = "structural finding implies a concrete refactor / extraction / module split"
|
||||
produces_artifact = "spec"
|
||||
|
||||
[[handoff]]
|
||||
target = "kei-critic"
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ trigger = "task involves deploy / CI/CD / secrets / IaC / credentials / public-s
|
|||
[[handoff]]
|
||||
target = "kei-critic"
|
||||
trigger = "anti-pattern sweep / code smell review on large diff (>500 LOC) or long function chains"
|
||||
produces_artifact = "patch"
|
||||
|
||||
[[handoff]]
|
||||
target = "kei-security-auditor"
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ produces_artifact = "review"
|
|||
target = "kei-code-implementer"
|
||||
trigger = "confirmed findings need code edits (user approves fix plan first)"
|
||||
expects_artifact = "patch"
|
||||
produces_artifact = "review"
|
||||
|
||||
[[handoff]]
|
||||
target = "kei-security-auditor"
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ output_extra_fields = [
|
|||
"9-point checklist coverage: [x]/[ ] per item",
|
||||
]
|
||||
|
||||
# v0.15.1: typed-artifact handoff — security-auditor emits a `review` artifact
|
||||
# with severity-sorted findings (schema: kei-artifact://review).
|
||||
produces_artifact = "review"
|
||||
|
||||
# Handoffs MUST come after all top-level keys (TOML array-of-tables scope rule)
|
||||
[[handoff]]
|
||||
target = "kei-code-implementer"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@ output_extra_fields = [
|
|||
"Overall verdict: ALL VERIFIED | PARTIAL (fix list) | BLOCK (FALSE findings present)",
|
||||
]
|
||||
|
||||
# v0.15.1: typed-artifact handoff — validator emits a `review` artifact
|
||||
# with severity-sorted findings (schema: kei-artifact://review).
|
||||
produces_artifact = "review"
|
||||
|
||||
# Handoffs MUST come after all top-level keys (TOML array-of-tables scope rule)
|
||||
[[handoff]]
|
||||
target = "kei-ml-researcher"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
},
|
||||
"findings": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,54 @@ pub fn validate_content(schema: &Value, content: &Value) -> Result<(), String> {
|
|||
check(schema, content, "$")
|
||||
}
|
||||
|
||||
/// Keywords the minimal validator knows about. Used by `warn_unsupported_keywords`
|
||||
/// to flag — but not reject — schemas that lean on unsupported features (so an
|
||||
/// operator writing human-readable docs in a schema still sees them stored,
|
||||
/// while being warned they do not actually enforce anything).
|
||||
const KNOWN_KEYWORDS: &[&str] = &[
|
||||
"$schema",
|
||||
"$id",
|
||||
"title",
|
||||
"description",
|
||||
"type",
|
||||
"required",
|
||||
"properties",
|
||||
"additionalProperties",
|
||||
"enum",
|
||||
"items",
|
||||
"minLength",
|
||||
"minItems",
|
||||
"minimum",
|
||||
];
|
||||
|
||||
/// Emit a stderr warning for each schema keyword this validator does not
|
||||
/// enforce. Non-fatal: the schema is still accepted and stored verbatim —
|
||||
/// operators can keep `pattern` / `format` / `oneOf` etc. as human-readable
|
||||
/// hints without expecting runtime validation of them.
|
||||
///
|
||||
/// Walks the schema recursively so a nested `items` / `properties` sub-schema
|
||||
/// with an unsupported keyword is caught too.
|
||||
pub fn warn_unsupported_keywords(schema: &Value) {
|
||||
fn walk(v: &Value, path: &str) {
|
||||
if let Value::Object(map) = v {
|
||||
for (k, sub) in map {
|
||||
if !KNOWN_KEYWORDS.contains(&k.as_str()) {
|
||||
eprintln!(
|
||||
"[kei-artifact] schema warning: unsupported keyword '{k}' at {path} — \
|
||||
stored but not enforced by the minimal validator (see validate.rs KNOWN_KEYWORDS)"
|
||||
);
|
||||
}
|
||||
walk(sub, &format!("{path}.{k}"));
|
||||
}
|
||||
} else if let Value::Array(arr) = v {
|
||||
for (i, el) in arr.iter().enumerate() {
|
||||
walk(el, &format!("{path}[{i}]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(schema, "$");
|
||||
}
|
||||
|
||||
fn check(schema: &Value, value: &Value, path: &str) -> Result<(), String> {
|
||||
if let Some(t) = schema.get("type") {
|
||||
check_type(t, value, path)?;
|
||||
|
|
@ -195,4 +243,26 @@ mod tests {
|
|||
let err = validate_content(&schema, &json!(["nope"])).unwrap_err();
|
||||
assert!(err.contains("enum"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warn_unsupported_keywords_does_not_panic_or_mutate() {
|
||||
// Smoke test — the warn function prints to stderr but returns unit and
|
||||
// never mutates the schema. We cannot portably capture stderr without
|
||||
// a gag-style helper, so we just assert execution is stable and the
|
||||
// schema is still usable by `validate_content` afterwards.
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"required": ["k"],
|
||||
"properties": {
|
||||
"k": {"type": "string", "pattern": "^[a-z]+$", "format": "email"}
|
||||
},
|
||||
"oneOf": [{"type": "object"}],
|
||||
"patternProperties": {"^x_": {"type": "string"}}
|
||||
});
|
||||
warn_unsupported_keywords(&schema);
|
||||
// Validator is still callable and still enforces the supported subset.
|
||||
assert!(validate_content(&schema, &json!({"k": "hi"})).is_ok());
|
||||
let err = validate_content(&schema, &json!({})).unwrap_err();
|
||||
assert!(err.contains("k"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -137,3 +137,22 @@ fn patch_schema_rejects_invalid_op_enum() {
|
|||
let msg = format!("{err:#}");
|
||||
assert!(msg.contains("enum"), "unexpected: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_schema_rejects_empty_findings() {
|
||||
// v0.15.1 HIGH fix: review artifacts must list ≥ 1 finding so a `reject`
|
||||
// or `request_changes` verdict cannot ship with nothing to point at.
|
||||
let s = seed();
|
||||
let bad = serde_json::to_vec(&json!({
|
||||
"reviewer": "kei-critic",
|
||||
"findings": [],
|
||||
"verdict": "reject"
|
||||
}))
|
||||
.unwrap();
|
||||
let err = emit(&s, "review", "kei-critic", &bad, None, None).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.contains("array") || msg.contains("min"),
|
||||
"unexpected: {msg}"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -13,16 +13,21 @@
|
|||
# Claude Code would refuse the tool call system-wide.
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -10,16 +10,21 @@
|
|||
# Claude Code would refuse the tool call system-wide.
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -12,16 +12,21 @@
|
|||
# Claude Code would refuse Edit/Write system-wide.
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -13,16 +13,21 @@
|
|||
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -11,16 +11,21 @@
|
|||
# Claude Code would refuse Read system-wide.
|
||||
command -v jq >/dev/null 2>&1 || exit 0
|
||||
|
||||
# --- RUNTIME CONTROLS (v0.14.2) ---
|
||||
# --- RUNTIME CONTROLS (v0.15.1) ---
|
||||
# KEI_DISABLED_HOOKS: tokenized exact-match list (comma- or space-separated).
|
||||
# Repro of pre-v0.15.1 substring bypass (CVE-class): KEI_DISABLED_HOOKS="foo-all-bar"
|
||||
# previously disabled every hook via `*all*`. v0.15.1 requires token equality.
|
||||
_hook_name="$(basename "$0" .sh)"
|
||||
case "${KEI_DISABLED_HOOKS:-}" in
|
||||
*"$_hook_name"*|*all*) exit 0 ;;
|
||||
_disabled=" $(printf '%s' "${KEI_DISABLED_HOOKS:-}" | tr ',' ' ') "
|
||||
case "$_disabled" in
|
||||
*" $_hook_name "*|*" all "*) unset _disabled; exit 0 ;;
|
||||
esac
|
||||
unset _disabled
|
||||
case "${KEI_HOOK_PROFILE:-full}" in
|
||||
off) exit 0 ;;
|
||||
minimal)
|
||||
case "$_hook_name" in
|
||||
no-github-push|genesis-leak-guard|no-hand-edit-agents|secrets-guard|assemble-validate|git-pre-commit-genesis) ;;
|
||||
no-hand-edit-agents|assemble-validate|agent-fork-logger|session-end-dump) ;;
|
||||
*) exit 0 ;;
|
||||
esac
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: hooks-control
|
||||
description: Runtime enable/disable of KeiSeiKit hooks via env vars (v0.14.2). Click-only wizard that emits shell `export` / `unset` commands for the user to paste. Supports per-hook disable, profile switch (full / advisory-off / minimal / off), or full re-enable. Does NOT execute anything — user controls their shell.
|
||||
description: Runtime enable/disable of KeiSeiKit hooks via env vars (v0.15.1). Click-only wizard that emits shell `export` / `unset` commands for the user to paste. Supports per-hook disable, profile switch (full / advisory-off / minimal / off), or full re-enable. Does NOT execute anything — user controls their shell.
|
||||
argument-hint: (none — fully click-driven)
|
||||
---
|
||||
|
||||
|
|
@ -10,18 +10,18 @@ Click-only wizard. Helps you toggle KeiSeiKit hooks **for the current shell
|
|||
session** via env vars, without editing `~/.claude/settings.json`. The skill
|
||||
emits shell commands; it NEVER runs them.
|
||||
|
||||
Two env vars are honoured by every kit-shipped hook (v0.14.2+):
|
||||
Two env vars are honoured by every kit-shipped hook (v0.15.1+):
|
||||
|
||||
| Var | Meaning |
|
||||
|---|---|
|
||||
| `KEI_DISABLED_HOOKS` | Comma- or space-list of hook base names (no `.sh`). `all` disables every hook. |
|
||||
| `KEI_DISABLED_HOOKS` | Comma- or space-list of hook base names (no `.sh`). Matching is **tokenized exact-match** (v0.15.1 fix — earlier versions used substring-glob, which let `foo-all-bar` disable every hook). The literal `all` token still disables every hook. |
|
||||
| `KEI_HOOK_PROFILE` | One of `full` (default), `advisory-off`, `minimal`, `off`. |
|
||||
|
||||
| Profile | What stays on |
|
||||
|---|---|
|
||||
| `full` (default) | Every hook |
|
||||
| `advisory-off` | Disables pure-stderr advisories: `recurrence-suggest`, `citation-verify`, `error-spike-detector`, `milestone-commit-hook`. |
|
||||
| `minimal` | Safety-only: `no-github-push`, `genesis-leak-guard`, `no-hand-edit-agents`, `secrets-guard`, `assemble-validate`, `git-pre-commit-genesis`. |
|
||||
| `minimal` | Only the four kit-shipped hooks needed for structural integrity or observability: `no-hand-edit-agents`, `assemble-validate`, `agent-fork-logger`, `session-end-dump`. User-global safety hooks (`no-github-push`, `secrets-guard`, `genesis-leak-guard`, `git-pre-commit-genesis`) are not shipped by the kit but are respected when present in `~/.claude/hooks/`. |
|
||||
| `off` | Every hook off (escape hatch — use when debugging hook interactions). |
|
||||
|
||||
---
|
||||
|
|
@ -34,7 +34,7 @@ Print current state:
|
|||
```
|
||||
Current KEI_DISABLED_HOOKS: <value or "(unset)">
|
||||
Current KEI_HOOK_PROFILE: <value or "full (default)">
|
||||
Active kit-shipped hooks: <list of 10 minus disabled set>
|
||||
Active kit-shipped hooks: <list of 9 minus disabled set>
|
||||
```
|
||||
|
||||
`AskUserQuestion` — **What do you want to do?**
|
||||
|
|
@ -45,10 +45,10 @@ Active kit-shipped hooks: <list of 10 minus disabled set>
|
|||
|
||||
### Phase 2a — Hook multi-select (if picked 1)
|
||||
|
||||
`AskUserQuestion` multi-select over the 10 kit-shipped hook names:
|
||||
`AskUserQuestion` multi-select over the 9 kit-shipped hook names:
|
||||
`assemble-agents`, `assemble-validate`, `no-hand-edit-agents`, `tomd-preread`,
|
||||
`agent-fork-logger`, `site-wysiwyd-check`, `error-spike-detector`,
|
||||
`milestone-commit-hook`, `session-end-dump`, `git-pre-commit-genesis`.
|
||||
`milestone-commit-hook`, `session-end-dump`.
|
||||
|
||||
Emit:
|
||||
```sh
|
||||
|
|
@ -88,7 +88,7 @@ Stop after the state block.
|
|||
exit — the shell running hooks is a subshell.
|
||||
- **No rc edits.** If the user wants persistence, we say "paste into your
|
||||
shell rc". The skill MUST NOT modify `~/.zshrc` / `~/.bashrc`.
|
||||
- **RULE 0.4 — no invented hook names.** Only the 10 names in Phase 2a
|
||||
- **RULE 0.4 — no invented hook names.** Only the 9 names in Phase 2a
|
||||
are valid choices. Never suggest a name not in the kit.
|
||||
- **RULE -1 — NO DOWNGRADE.** If the user asks "can I silence all safety
|
||||
hooks?", present tradeoffs; point at `KEI_HOOK_PROFILE=off` with a
|
||||
|
|
@ -111,7 +111,7 @@ Undo: unset KEI_DISABLED_HOOKS KEI_HOOK_PROFILE
|
|||
|
||||
## References
|
||||
|
||||
- `hooks/*.sh` — each kit hook sources the v0.14.2 runtime-controls block
|
||||
- `hooks/*.sh` — each kit hook sources the v0.15.1 runtime-controls block
|
||||
- `README.md` → "Runtime hook controls" section
|
||||
- `~/.claude/rules/recurrence-escalate.md` — severity ladder notes that
|
||||
hooks can be silenced at runtime, no rule deletion required
|
||||
|
|
|
|||
Loading…
Reference in a new issue