agent-quality-kit 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -2
- package/README.ru.md +61 -2
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/_skip.sh +61 -1
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +27 -9
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +36 -5
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- package/kit/gates/gate-not-weakened/check.sh +13 -1
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/secrets-not-in-code/check.sh +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/check.sh +13 -1
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +9 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +122 -0
- package/tool/selfcheck/units.mjs +114 -2
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
|
@@ -16,7 +16,14 @@
|
|
|
16
16
|
**Чего НЕ ловит.** Длину функции и вложенность: файл на 200 строк с одной функцией в 180 строк
|
|
17
17
|
проверку пройдёт. Это отдельная мера — цикломатическая сложность.
|
|
18
18
|
|
|
19
|
-
**Готового аналога
|
|
20
|
-
размер файла нет ни
|
|
19
|
+
**Готового аналога нет — для Python.** Проверено дважды, второй раз 2026-09-07 по каталогу
|
|
20
|
+
правил `ruff`: предела на размер файла там нет ни одного (`pylint C0302 too-many-lines` в `ruff`
|
|
21
|
+
не перенесён). В eslint правило `max-lines` есть, но рецепта под него здесь нет намеренно: оно
|
|
22
|
+
знает один предел на все файлы, а запись держит два — 500 строк прод-коду и 800 тесту. Рецепт,
|
|
23
|
+
который меряет не то же самое, что переносимая проверка, делает запись означающей разное на
|
|
24
|
+
разных машинах; правило каталога это запрещает (см. `color-from-token`).
|
|
25
|
+
|
|
26
|
+
**Замер.** Пять чужих репозиториев (`httpx`, `fastapi`, `zod`, `cobra`, `ripgrep`) — 86 находок,
|
|
27
|
+
ложных **ноль**: мера прямая, файл либо длиннее предела, либо нет.
|
|
21
28
|
|
|
22
29
|
**Образцы.** `red/` — прод-файл на 600 строк. `green/` — тот же код, разделённый надвое.
|
|
@@ -5,13 +5,25 @@
|
|
|
5
5
|
# не по замыслу: каждую новую фичу дописывают в тот же файл, потому что агенту так ближе по
|
|
6
6
|
# контексту. Числа спорные — важно, что предел существует и его считает машина.
|
|
7
7
|
DIR="${1:-.}"
|
|
8
|
-
|
|
8
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
9
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
10
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
11
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
12
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
13
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
14
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
15
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
16
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
17
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
. "$SKIP_LIB"
|
|
9
21
|
|
|
10
22
|
# Один обход и один wc на все файлы разом: на проекте в 36 тысяч файлов цикл с wc на каждый
|
|
11
23
|
# не укладывался в две минуты.
|
|
12
24
|
# shellcheck disable=SC2046
|
|
13
25
|
find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
|
|
14
|
-
|
|
|
26
|
+
| drop_generated \
|
|
15
27
|
| xargs -r wc -l 2>/dev/null \
|
|
16
28
|
| awk '
|
|
17
29
|
$2 == "total" { next }
|
|
@@ -11,7 +11,19 @@
|
|
|
11
11
|
# гейт, который выключат. Одно исключение — `@ts-ignore`: у него есть строго лучший брат
|
|
12
12
|
# `@ts-expect-error`, который сам краснеет, когда становится не нужен.
|
|
13
13
|
DIR="${1:-.}"
|
|
14
|
-
|
|
14
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
15
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
16
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
17
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
18
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
19
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
20
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
21
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
22
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
23
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
24
|
+
exit 2
|
|
25
|
+
fi
|
|
26
|
+
. "$SKIP_LIB"
|
|
15
27
|
|
|
16
28
|
OUT=""
|
|
17
29
|
add() { [ -z "$1" ] || OUT="$OUT$1
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Хук агента правда срабатывает
|
|
2
|
+
|
|
3
|
+
**Намерение.** Человек заводит хук, чтобы машина держала то, что он держать не может: не дать
|
|
4
|
+
сделать `git push --force`, отформатировать после правки, не отпустить работу с красным
|
|
5
|
+
линтером. Ошибку в имени события Claude Code **не показывает** — хук просто никогда не
|
|
6
|
+
вызывается. Настройка выглядит как защита и защитой не является.
|
|
7
|
+
|
|
8
|
+
**Какой отказ это поймало.** Замер по 48 чужим настройкам `.claude/settings.json`, снятым с
|
|
9
|
+
GitHub 2026-09-07. Шесть настоящих находок в четырёх репозиториях:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
"PreToolCall": [
|
|
13
|
+
{ "matcher": "Bash(git commit*)",
|
|
14
|
+
"command": "npm run typecheck && npm run test",
|
|
15
|
+
"description": "Run typecheck and tests before committing to ensure CI checks will pass" }
|
|
16
|
+
]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
События `PreToolCall` у Claude Code нет. Человек написал в описании, ради чего это стоит, — и не
|
|
20
|
+
запускается оно никогда. Такие же `PostToolCall` и `preToolCall` нашлись ещё в трёх проектах.
|
|
21
|
+
|
|
22
|
+
**Вторая половина записи — `matcher`, которого нет.** Документация дословно: *«If you add a
|
|
23
|
+
`matcher` field to an event without matcher support, it is silently ignored.»* События без
|
|
24
|
+
поддержки `matcher` — `UserPromptSubmit`, `PostToolBatch`, `Stop`, `TeammateIdle`, `TaskCreated`,
|
|
25
|
+
`TaskCompleted`, `WorktreeCreate`, `WorktreeRemove`, `MessageDisplay`, `CwdChanged`. Написав
|
|
26
|
+
`"Stop": [{ "matcher": "Bash" }]`, автор думает, что отобрал, а хук срабатывает на всём.
|
|
27
|
+
|
|
28
|
+
**Готовый аналог есть, и он этого не ловит — проверено прогоном.**
|
|
29
|
+
[`claudelint`](https://github.com/pdugan20/claudelint) (npm `claude-code-lint`, MIT, обновлялся
|
|
30
|
+
вчера) проверяет схему `settings.json`, синтаксис прав и ловит `"allow": ["*"]` — это мы не
|
|
31
|
+
дублируем, ставьте его рядом. В его исходниках есть и правило `hooks-invalid-event`, но живой
|
|
32
|
+
прогон версии 0.8.0 на файле с `"PoToolUse"` даёт «No problems found»: категория `hooks` на
|
|
33
|
+
`.claude/settings.json` не срабатывает. Причина найдена в их исходнике 2026-09-08 — правила
|
|
34
|
+
категории `Hooks` запускает только `HooksValidator` (`src/validators/hooks.ts`), а он ищет файлы
|
|
35
|
+
по единственному образцу `hooks/hooks.json` (`src/utils/filesystem/patterns.ts:45`). Сообщено:
|
|
36
|
+
[issue #217](https://github.com/pdugan20/claudelint/issues/217). [`cclint`](https://github.com/carlrannaberg/cclint)
|
|
37
|
+
последний раз трогали в сентябре 2025 и лицензии у него нет. `AgentLint` знает 12 событий из 33.
|
|
38
|
+
|
|
39
|
+
**Почему список, а не «похоже на опечатку».** Первая версия краснела только на близких промахах —
|
|
40
|
+
чтобы не объявить опечаткой событие, появившееся после нас. Замер эту конструкцию убил:
|
|
41
|
+
`PreToolCall` отстоит от `PreToolUse` на четыре правки, и проверка его пропускала. Правило,
|
|
42
|
+
красящее **всё** незнакомое, на 39 случайных чужих настройках дало **ноль** ложных.
|
|
43
|
+
|
|
44
|
+
Размен назван вслух: событие, добавленное в Claude Code после нас, эта проверка объявит
|
|
45
|
+
незнакомым. Такое ложное срабатывание видно, оно громкое и чинится одной строкой в списке.
|
|
46
|
+
Пропуск не виден никак — хук молчит, и молчание неотличимо от того, что всё хорошо. Из двух
|
|
47
|
+
ошибок выбираем шумную.
|
|
48
|
+
|
|
49
|
+
**Откуда список.** https://code.claude.com/docs/en/hooks — раздел Configuration, таблица
|
|
50
|
+
«Each event type matches on a different field» и заголовки разделов событий. Снято 2026-09-07,
|
|
51
|
+
33 события. Дата стоит в самой проверке: список устареет, и это должно быть видно.
|
|
52
|
+
|
|
53
|
+
**Чего НЕ ловит.**
|
|
54
|
+
|
|
55
|
+
- **Хук с верным именем, который падает или ничего не делает.** Проверяется только то, что он
|
|
56
|
+
будет вызван, а не то, что он работает. Вызванный и сломанный хук — предмет другой записи.
|
|
57
|
+
- **Права.** `"allow": ["*"]`, слишком широкие разрешения, ошибки в синтаксисе правил — это
|
|
58
|
+
`claudelint`, и он делает это лучше.
|
|
59
|
+
- **Команду хука, указывающую на несуществующий скрипт.** Искали и **не нашли материала**:
|
|
60
|
+
шесть чужих репозиториев склонированы целиком, 30 путей к скриптам хуков проверены на диске,
|
|
61
|
+
несуществующих — ноль. Это не «не искали»: выборка содержала предмет, и предмет оказался
|
|
62
|
+
здоров. Запись не начата. У `claudelint` правило `hooks-missing-script` есть, но на
|
|
63
|
+
`.claude/settings.json` оно, как и `hooks-invalid-event`, не срабатывает — проверено прогоном
|
|
64
|
+
версии 0.8.0.
|
|
65
|
+
- **Хуки в плагинах и в `~/.claude/`.** Смотрим только настройки проекта: домашний каталог не
|
|
66
|
+
лежит в репозитории и в дифе не виден.
|
|
67
|
+
- **`stop_hook_active`.** Хук на `Stop`, не проверяющий это поле, зациклится — но Claude Code
|
|
68
|
+
сам обрывает его после восьми подряд блокировок, так что цена ошибки восемь ходов, а не
|
|
69
|
+
вечность. Проверять содержимое скрипта на любом языке дороже, чем стоит.
|
|
70
|
+
|
|
71
|
+
**Образцы.** `red/` — опечатка (`PoToolUse`), чужое написание (`post_tool_use`), незнакомое имя
|
|
72
|
+
(`PreToolCall`) и `matcher` на `Stop`. `green/` — те же хуки с верными именами, `matcher: ""` и
|
|
73
|
+
`matcher: "*"` на событиях без поддержки matcher (они означают «всё» — ровно то, что и
|
|
74
|
+
происходит, автор не обманут).
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Хук, который не сработает никогда: имя события с опечаткой, либо `matcher` на событии,
|
|
3
|
+
# которое его не поддерживает.
|
|
4
|
+
#
|
|
5
|
+
# ЗАЧЕМ. Человек заводит хук, чтобы машина держала то, что он держать не может: не дать
|
|
6
|
+
# сделать force-push, отформатировать после правки, не отпустить работу с красным линтером.
|
|
7
|
+
# Ошибку в имени события Claude Code НЕ показывает — хук просто никогда не вызывается.
|
|
8
|
+
# Настройка выглядит как защита и защитой не является. Это тот же класс, что `pytest || true`:
|
|
9
|
+
# зелёное, полученное по причине, не имеющей отношения к предмету.
|
|
10
|
+
#
|
|
11
|
+
# ОТКУДА СПИСОК СОБЫТИЙ. https://code.claude.com/docs/en/hooks — раздел Configuration, таблица
|
|
12
|
+
# «Each event type matches on a different field» и заголовки разделов. Снято 2026-09-07.
|
|
13
|
+
# Дословно оттуда же про matcher: "If you add a `matcher` field to an event without matcher
|
|
14
|
+
# support, it is silently ignored."
|
|
15
|
+
#
|
|
16
|
+
# ПОЧЕМУ ВСЁ-ТАКИ СПИСОК, А НЕ «БЛИЗКИЙ ПРОМАХ». Первая версия краснела только на именах,
|
|
17
|
+
# отличающихся от известного одной-двумя буквами: список стареет, и не хотелось объявлять
|
|
18
|
+
# опечаткой событие, появившееся после нас. Замер эту конструкцию убил. В `kevinreber/watch-party`
|
|
19
|
+
# лежит `"PreToolCall"` — хук, гоняющий typecheck и тесты перед `git commit` и `git push`,
|
|
20
|
+
# который не срабатывает никогда. От `PreToolUse` это имя отстоит на четыре правки, и проверка
|
|
21
|
+
# по близости его пропускала. Правило же, красящее всё незнакомое, на 39 чужих настройках дало
|
|
22
|
+
# ноль ложных.
|
|
23
|
+
#
|
|
24
|
+
# Размен назван вслух: событие, добавленное в Claude Code после нас, эта проверка объявит
|
|
25
|
+
# незнакомым. Это ложное срабатывание — видимое, громкое и чинится одной строкой здесь. Пропуск
|
|
26
|
+
# же не виден никак: хук молчит, и молчание неотличимо от того, что всё хорошо. Из двух ошибок
|
|
27
|
+
# мы выбираем шумную.
|
|
28
|
+
DIR="${1:-.}"
|
|
29
|
+
|
|
30
|
+
# ПО ОДНОМУ ФАЙЛУ ЗА ПРОХОД, а не списком в один awk. Разбор идёт в END по накопленному тексту:
|
|
31
|
+
# при нескольких файлах в общем буфере FILENAME остаётся последним, а счётчик строк — сквозным,
|
|
32
|
+
# и находка из `settings.json` печаталась как `settings.local.json:71` вместо `settings.json:8`.
|
|
33
|
+
# Мало того что путь чужой: `--since` сверяет напечатанный путь с дифом, не находит его и
|
|
34
|
+
# считает находок ноль — гейт зеленеет. Найдено код-ревью 2026-09-07.
|
|
35
|
+
OUT=""
|
|
36
|
+
ERR=""
|
|
37
|
+
FOUND=0
|
|
38
|
+
for F in "$DIR/.claude/settings.json" "$DIR/.claude/settings.local.json" \
|
|
39
|
+
"$DIR/.claude/hooks/hooks.json" "$DIR/.claude/hooks.json"; do
|
|
40
|
+
[ -f "$F" ] || continue
|
|
41
|
+
FOUND=1
|
|
42
|
+
|
|
43
|
+
# Файл хуков без обёртки `"hooks": { … }` держит карту событий прямо в корне. Такой уклад
|
|
44
|
+
# встречается в `hooks.json`, и без этой поправки триггер срабатывал, а проверять было нечего:
|
|
45
|
+
# тишина неотличима от чистой настройки. В `settings.json` корневые ключи — `permissions`,
|
|
46
|
+
# `env`, `model`, — и считать их событиями нельзя, поэтому послабление только для hooks.json.
|
|
47
|
+
ROOT=0
|
|
48
|
+
# Ищем именно ОБЪЕКТ на верхнем уровне: `"hooks": {`. Просто `"hooks":` не годится — этот же
|
|
49
|
+
# ключ стоит внутри каждой группы («"matcher": "Bash", "hooks": [ … ]»), и грубый греп находил
|
|
50
|
+
# его всегда, из-за чего поблажка не включалась никогда. Найдено прогоном образца.
|
|
51
|
+
case "$F" in
|
|
52
|
+
*hooks.json) tr -d '\n' < "$F" | grep -q '"hooks"[[:space:]]*:[[:space:]]*{' || ROOT=1 ;;
|
|
53
|
+
esac
|
|
54
|
+
|
|
55
|
+
RES=$(awk -v rootIsHooks="$ROOT" '
|
|
56
|
+
BEGIN {
|
|
57
|
+
split("SessionStart Setup InstructionsLoaded UserPromptSubmit UserPromptExpansion " \
|
|
58
|
+
"MessageDisplay PreToolUse PermissionRequest PermissionDenied PostToolUse " \
|
|
59
|
+
"PostToolUseFailure PostToolBatch Notification SubagentStart SubagentStop " \
|
|
60
|
+
"TaskCreated TaskCompleted Stop StopFailure TeammateIdle ConfigChange CwdChanged " \
|
|
61
|
+
"DirectoryAdded FileChanged WorktreeCreate WorktreeRemove PreCompact PostCompact " \
|
|
62
|
+
"PreModelSwitch PostModelSwitch Elicitation ElicitationResult SessionEnd", KNOWN, " ")
|
|
63
|
+
# События, у которых matcher не поддерживается вовсе: он молча игнорируется, и хук
|
|
64
|
+
# срабатывает на каждом событии — шире, чем думает автор.
|
|
65
|
+
split("UserPromptSubmit PostToolBatch Stop TeammateIdle TaskCreated TaskCompleted " \
|
|
66
|
+
"WorktreeCreate WorktreeRemove MessageDisplay CwdChanged", NOMATCH, " ")
|
|
67
|
+
for (i in NOMATCH) NOMATCHER[NOMATCH[i]] = 1
|
|
68
|
+
for (i in KNOWN) NORM[normalize(KNOWN[i])] = KNOWN[i]
|
|
69
|
+
}
|
|
70
|
+
function normalize(s, t) { t = tolower(s); gsub(/[^a-z0-9]/, "", t); return t }
|
|
71
|
+
function min3(a, b, c) { if (a <= b && a <= c) return a; if (b <= c) return b; return c }
|
|
72
|
+
function edit(a, b, la, lb, i, j, prev, cur, cost) {
|
|
73
|
+
la = length(a); lb = length(b)
|
|
74
|
+
if (la == 0) return lb
|
|
75
|
+
if (lb == 0) return la
|
|
76
|
+
for (j = 0; j <= lb; j++) prev[j] = j
|
|
77
|
+
for (i = 1; i <= la; i++) {
|
|
78
|
+
cur[0] = i
|
|
79
|
+
for (j = 1; j <= lb; j++) {
|
|
80
|
+
cost = (substr(a, i, 1) == substr(b, j, 1)) ? 0 : 1
|
|
81
|
+
cur[j] = min3(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
|
|
82
|
+
}
|
|
83
|
+
for (j = 0; j <= lb; j++) prev[j] = cur[j]
|
|
84
|
+
}
|
|
85
|
+
return prev[lb]
|
|
86
|
+
}
|
|
87
|
+
function judge(ev, ln, nz, best, bestName, d, i) {
|
|
88
|
+
for (i in KNOWN) if (KNOWN[i] == ev) return
|
|
89
|
+
nz = normalize(ev)
|
|
90
|
+
if (nz in NORM) {
|
|
91
|
+
printf "%s:%d: событие «%s» написано не так, как его зовёт Claude Code — «%s». Хук не сработает никогда\n", file, ln, ev, NORM[nz]
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
best = 99; bestName = ""
|
|
95
|
+
for (i in KNOWN) { d = edit(nz, normalize(KNOWN[i])); if (d < best) { best = d; bestName = KNOWN[i] } }
|
|
96
|
+
if (best <= 3)
|
|
97
|
+
printf "%s:%d: событие «%s» Claude Code не знает — похоже на «%s». Хук не сработает никогда\n", file, ln, ev, bestName
|
|
98
|
+
else
|
|
99
|
+
printf "%s:%d: событие «%s» Claude Code не знает — хук не сработает никогда. Либо опечатка, либо событие новее этой проверки (список снят 2026-09-07)\n", file, ln, ev
|
|
100
|
+
}
|
|
101
|
+
# Разбор посимвольный с учётом строк и экранирования: `{` внутри строкового значения не
|
|
102
|
+
# меняет глубину. Построчный греп здесь врал бы на любом однострочном json.
|
|
103
|
+
FNR == 1 { file = FILENAME }
|
|
104
|
+
{ text = text $0 "\n" }
|
|
105
|
+
END {
|
|
106
|
+
n = length(text); depth = 0; instr = 0; esc = 0; line = 1
|
|
107
|
+
hooksDepth = -1; evDepth = -1; ev = ""; key = ""; buf = ""; awaitMatcher = 0
|
|
108
|
+
for (p = 1; p <= n; p++) {
|
|
109
|
+
c = substr(text, p, 1)
|
|
110
|
+
if (c == "\n") { line++; continue }
|
|
111
|
+
if (instr) {
|
|
112
|
+
if (esc) { esc = 0; buf = buf c; continue }
|
|
113
|
+
if (c == "\\") { esc = 1; continue }
|
|
114
|
+
if (c == "\"") {
|
|
115
|
+
instr = 0; pending = buf; pendingLine = line
|
|
116
|
+
# Значение matcher прочитано. Красим ТОЛЬКО осмысленный фильтр: пустая строка и «*»
|
|
117
|
+
# означают «всё» — ровно то, что и происходит на событии без поддержки matcher, то
|
|
118
|
+
# есть автор не обманут. Замер по 39 чужим настройкам: без этого сужения гейт краснел
|
|
119
|
+
# на четырёх, и все четыре были «matcher»: "" либо "*", то есть шум.
|
|
120
|
+
if (awaitMatcher) {
|
|
121
|
+
awaitMatcher = 0
|
|
122
|
+
if (pending != "" && pending != "*" && pending != ".*")
|
|
123
|
+
printf "%s:%d: у события «%s» matcher не поддерживается — «%s» молча игнорируется, и хук срабатывает на каждом событии, а не на отобранных\n", file, matcherLine, matcherEv, pending
|
|
124
|
+
}
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
buf = buf c; continue
|
|
128
|
+
}
|
|
129
|
+
if (c == "\"") { instr = 1; buf = ""; continue }
|
|
130
|
+
if (c == ":") {
|
|
131
|
+
key = pending; keyLine = pendingLine
|
|
132
|
+
if (ev != "" && key == "matcher" && (ev in NOMATCHER)) { awaitMatcher = 1; matcherLine = keyLine; matcherEv = ev }
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
# ЛЮБОЙ структурный символ снимает ожидание значения matcher. Без этого нестроковое
|
|
136
|
+
# значение (`"matcher": null`, число, массив) оставляло флаг взведённым, и первая же
|
|
137
|
+
# следующая строка документа — обычно ключ «hooks» — печаталась как значение matcher.
|
|
138
|
+
# Найдено код-ревью 2026-09-07.
|
|
139
|
+
if (c == "{" || c == "[") {
|
|
140
|
+
awaitMatcher = 0
|
|
141
|
+
depth++
|
|
142
|
+
if (c == "{" && rootIsHooks == 1 && depth == 1 && hooksDepth == -1) hooksDepth = 1
|
|
143
|
+
else if (c == "{" && key == "hooks" && hooksDepth == -1) hooksDepth = depth
|
|
144
|
+
else if (hooksDepth != -1 && depth == hooksDepth + 1 && key != "") { ev = key; evDepth = depth; judge(key, keyLine) }
|
|
145
|
+
key = ""; continue
|
|
146
|
+
}
|
|
147
|
+
if (c == "}" || c == "]") {
|
|
148
|
+
awaitMatcher = 0
|
|
149
|
+
if (depth == evDepth) { ev = ""; evDepth = -1 }
|
|
150
|
+
if (depth == hooksDepth) hooksDepth = -1
|
|
151
|
+
depth--; key = ""; continue
|
|
152
|
+
}
|
|
153
|
+
if (c == ",") { awaitMatcher = 0; key = ""; continue }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
' "$F" 2>/tmp/.hookerr.$$)
|
|
157
|
+
CODE=$?
|
|
158
|
+
E=$(cat /tmp/.hookerr.$$ 2>/dev/null); rm -f /tmp/.hookerr.$$
|
|
159
|
+
# Отказ инструмента и чистая настройка дают одинаково пустой вывод и противоположные выводы.
|
|
160
|
+
# Разделяем их кодом возврата: «не смогли разобрать» — это 2, а не молчаливый ноль.
|
|
161
|
+
if [ "$CODE" -ne 0 ] || [ -n "$E" ]; then
|
|
162
|
+
ERR="$ERR$F: разобрать не удалось${E:+ — }$E
|
|
163
|
+
"
|
|
164
|
+
fi
|
|
165
|
+
[ -n "$RES" ] && OUT="$OUT$RES
|
|
166
|
+
"
|
|
167
|
+
done
|
|
168
|
+
|
|
169
|
+
[ "$FOUND" -eq 0 ] && exit 0
|
|
170
|
+
|
|
171
|
+
if [ -n "$ERR" ]; then
|
|
172
|
+
printf '%s' "$ERR"
|
|
173
|
+
echo " почини: покажи файл настроек глазами — проверка не смогла его разобрать."
|
|
174
|
+
echo " «не смогли проверить» и «нарушений нет» дают одинаково пустой список и разные выводы."
|
|
175
|
+
exit 2
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
ALL=$(printf '%s' "$OUT" | grep -v '^$')
|
|
179
|
+
[ -z "$ALL" ] && exit 0
|
|
180
|
+
printf '%s\n' "$ALL"
|
|
181
|
+
echo " почини: сверь имя события с https://code.claude.com/docs/en/hooks и убери matcher там, где его нет."
|
|
182
|
+
echo " хук с неверным именем не вызывается и об этом не сообщается — защита существует только на бумаге."
|
|
183
|
+
exit 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
intent: хук агента правда срабатывает — имя события известно, matcher не игнорируется молча
|
|
2
|
+
intent_en: an agent hook actually fires — the event name is real and the matcher is not silently ignored
|
|
3
|
+
|
|
4
|
+
# Только там, где агента настраивали. В проекте без `.claude/settings.json` проверять нечего,
|
|
5
|
+
# а запись, показанная не тому, стоит доверия всему каталогу.
|
|
6
|
+
trigger:
|
|
7
|
+
has_agent_config: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
proof: incidents/README.md, 2026-09-07 «хук, которого никогда не было» — замер по 48 чужим
|
|
13
|
+
настройкам: ноль ложных на 39 случайных и шесть настоящих находок в четырёх репозиториях
|
|
14
|
+
(`PreToolCall`, `PostToolCall`, `preToolCall` — событий с такими именами у Claude Code нет,
|
|
15
|
+
и хуки, гоняющие typecheck и тесты перед коммитом, не срабатывают там никогда)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(./.env)"
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/block-dangerous.sh"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"PostToolUse": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Write|Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": ".claude/hooks/auto-format.sh"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"Stop": [
|
|
31
|
+
{
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"command": ".claude/hooks/stop-gate.sh"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"UserPromptSubmit": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": ".claude/hooks/prompt.sh"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"TaskCompleted": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "*",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": ".claude/hooks/done.sh"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"TeammateIdle": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": null,
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": ".claude/hooks/idle.sh"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(./.env)"
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/block-dangerous.sh"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"PostToolUse": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Write|Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": ".claude/hooks/auto-format.sh"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"Stop": [
|
|
31
|
+
{
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"command": ".claude/hooks/stop-gate.sh"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"UserPromptSubmit": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": ".claude/hooks/prompt.sh"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"TaskCompleted": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "*",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": ".claude/hooks/done.sh"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"TeammateIdle": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": null,
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": ".claude/hooks/idle.sh"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(./.env)"
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PoToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/block-dangerous.sh"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"post_tool_use": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Write|Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": ".claude/hooks/auto-format.sh"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"Stop": [
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": ".claude/hooks/stop-gate.sh"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"PreToolCall": [
|
|
42
|
+
{
|
|
43
|
+
"matcher": "Bash(git commit*)",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "npm run typecheck && npm test"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|