Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).

## [0.62.0] - 2026-06-12

LLM 性能監査(#121)P2 の第 2 弾(#112)。doc-blocker を allowlist 全拒否から slop パターン拒否方式に反転した。

### Changed
- **doc-blocker を deny-by-pattern 方式に反転(#112)**: 旧実装は allowlist 外の全 .md/.txt Write を exit 2 で無条件ブロックし、ユーザーが明示的に依頼した文書(CHANGELOG.md・LICENSE.txt・docs/ 配下の設計文書・ADR 等)まで一律に妨げていた。新実装: (1) SUMMARY / REPORT / FINDINGS / ANALYSIS / NOTES / RESULTS / TAKEAWAYS 系のアドホック文書名(大文字小文字不問・アンダースコア結合変形含む)のみ対象、(2) ブロックではなく PreToolUse の `permissionDecision: ask` で確認に降格(ユーザー依頼なら 1 確認で通せる)、(3) 確認メッセージに対象パス・常時許可パス・無効化手段(ENABLE_DOC_BLOCKER=false)を明記、(4) キットコマンドの出力先 allowlist(HANDOVER.md / .reports/ / docs/CODEMAPS/ 等)は「常時許可」として維持。一般文書は素通しになり、exit 2 のハードブロックは全廃

## [0.61.0] - 2026-06-12

LLM 性能監査(#121)P2 の第 1 弾(#111)。tmux-hooks のハードブロックをリマインダーに降格した。
Expand Down
2 changes: 1 addition & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ Hooks are automated safety checks that run automatically when Claude Code execut
| **Auto Update** | Checks for starter kit updates on session start and applies them in the background |
| Tmux Reminder | Suggests run_in_background for foreground dev servers (non-blocking) |
| Git Push Review | Pauses before git push for code review |
| Doc Blocker | Prevents creation of unnecessary .md/.txt files |
| Doc Blocker | Asks for confirmation only on ad-hoc SUMMARY/REPORT style docs (general docs pass) |
| Prettier Auto-format | Formats JS/TS files after edits |
| Biome Auto-format | Formats and lints JS/TS files after edits (Full uses Biome instead of Prettier) |
| Console.log Guard | Warns about console.log statements left in code |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ URL・公式ドキュメント・ブログ・ニュース・OSS ページを読
| **自動アップデート** | **セッション開始時にスターターキットの最新版を自動で適用** |
| Tmux リマインダー | foreground の dev サーバーに run_in_background を提案(非ブロック) |
| Git Push レビュー | コードを共有する前に確認を促す |
| Doc ブロッカー | 不要なドキュメントファイルの作成を防止 |
| Doc ブロッカー | SUMMARY/REPORT 等のアドホック文書のみ確認に降格(一般文書は素通し) |
| Prettier 自動フォーマット | JS/TS ファイルを自動で見た目を整える |
| Biome 自動フォーマット | JS/TS ファイルを整形・lint(Full は Prettier の代わりに Biome) |
| Console.log ガード | デバッグ用コードの消し忘れを警告 |
Expand Down
2 changes: 1 addition & 1 deletion docs/wizard-config-mapping.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ These flags are used to merge the corresponding `features/*/hooks.json` fragment
| `ENABLE_WEB_CONTENT_UPDATE` | Web Content Update | Check web extraction skill dependency updates | Yes |
| `ENABLE_TMUX_HOOKS` | Tmux Reminder | Suggest run_in_background for foreground dev servers (non-blocking) | Yes |
| `ENABLE_GIT_PUSH_REVIEW` | Git Push Review | Pause before push and open a diff | Yes |
| `ENABLE_DOC_BLOCKER` | Doc Blocker | Prevent unnecessary `.md` / `.txt` files | Yes |
| `ENABLE_DOC_BLOCKER` | Doc Blocker | Ask-only guard for slop doc patterns (general docs allowed) | Yes |
| `ENABLE_PRETTIER_HOOKS` | Prettier Auto-format | Format JS / TS edits | Yes |
| `ENABLE_BIOME_HOOKS` | Biome Auto-format | Format and lint JS / TS edits | Yes |
| `ENABLE_CONSOLE_LOG_GUARD` | Console Log Guard | Warn on leftover `console.log` | Yes |
Expand Down
2 changes: 1 addition & 1 deletion docs/wizard-config-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
| `ENABLE_WEB_CONTENT_UPDATE` | Web Content Update | Web 取得スキル依存の更新確認 | はい |
| `ENABLE_TMUX_HOOKS` | Tmux Reminder | foreground の dev サーバーに run_in_background を提案(非ブロック) | はい |
| `ENABLE_GIT_PUSH_REVIEW` | Git Push Review | push 前に差分確認 | はい |
| `ENABLE_DOC_BLOCKER` | Doc Blocker | 不要な `.md` / `.txt` 作成を抑制 | はい |
| `ENABLE_DOC_BLOCKER` | Doc Blocker | SUMMARY/REPORT 等の slop パターンのみ確認に降格(一般文書は許可) | はい |
| `ENABLE_PRETTIER_HOOKS` | Prettier Auto-format | JS / TS 編集後の整形 | はい |
| `ENABLE_BIOME_HOOKS` | Biome Auto-format | JS / TS 編集後の整形・lint | はい |
| `ENABLE_CONSOLE_LOG_GUARD` | Console Log Guard | `console.log` の取り残し検知 | はい |
Expand Down
8 changes: 6 additions & 2 deletions features/doc-blocker/feature.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
{
"name": "doc-blocker",
"displayName": "Doc File Blocker",
"description": "Block creation of random .md/.txt files to keep docs consolidated",
"description": "Ask for confirmation when writing ad-hoc SUMMARY/REPORT style docs; general documentation passes through",
"category": "hook",
"default": false,
"profiles": { "minimal": false, "standard": false, "full": true },
"profiles": {
"minimal": false,
"standard": false,
"full": true
},
"dependencies": [],
"conflicts": []
}
31 changes: 28 additions & 3 deletions features/doc-blocker/scripts/check-doc-write.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#!/bin/bash
# Block ad-hoc docs while allowing documented Starter Kit output locations.
# Deny-by-pattern doc guard: only ad-hoc summary/report style docs trigger a
# confirmation; everything else (CHANGELOG.md, LICENSE.txt, docs/**, ADRs, ...)
# passes through untouched.
set -euo pipefail

input="$(cat)"
file_path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // ""')"

# Kit command/agent output locations: always allowed, even slop-like names.
_doc_blocker_allowed_path() {
local path="$1"
case "$path" in
Expand All @@ -19,9 +22,31 @@ _doc_blocker_allowed_path() {
return 1
}

# Ad-hoc "model slop" doc names: SUMMARY.md, FINAL_REPORT.md, analysis.md,
# NOTES_2026.txt, ... (case-insensitive; underscore-joined variants included).
_doc_blocker_slop_name() {
local base="$1"
local matched=1
shopt -s nocasematch
if [[ "$base" =~ ^(.*_)?(SUMMARY|REPORT|FINDINGS|ANALYSIS|NOTES|RESULTS|TAKEAWAYS)(_.*)?\.(md|txt)$ ]]; then
matched=0
fi
shopt -u nocasematch
return "$matched"
}
Comment on lines +27 to +36

if [[ "$file_path" =~ \.(md|txt)$ ]] && ! _doc_blocker_allowed_path "$file_path"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match uppercase doc extensions before allowing slop files

When the Write target is a slop filename with an uppercase extension such as SUMMARY.MD or NOTES.TXT, this outer case-sensitive \.(md|txt)$ guard fails before _doc_blocker_slop_name can run its nocasematch regex, so the hook falls through and allows the write instead of returning the intended permissionDecision: "ask". This leaves a simple filename casing bypass for the new case-insensitive deny-by-pattern behavior.

Useful? React with 👍 / 👎.

echo "[Hook] BLOCKED: Use README.md or an approved Starter Kit output path for documentation" >&2
exit 2
base="$(basename "$file_path")"
if _doc_blocker_slop_name "$base"; then
jq -cn --arg path "$file_path" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: ("doc-blocker: \($path) matches an ad-hoc summary/report doc pattern (SUMMARY/REPORT/FINDINGS/ANALYSIS/NOTES/RESULTS/TAKEAWAYS). Approve only if the user explicitly asked for this file. Kit output paths (HANDOVER.md, .reports/, docs/CODEMAPS/, ...) are always allowed; the guard can be disabled with ENABLE_DOC_BLOCKER=false.")
}
}'
exit 0
fi
fi

printf '%s\n' "$input"
2 changes: 1 addition & 1 deletion i18n/en/strings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ STR_HOOKS_AUTO_UPDATE="Auto Update - Automatically update starter kit on session
STR_HOOKS_WEB_CONTENT_UPDATE="Web Content Update - Auto-update web-content-extraction skill deps on session start (opt-in)"
STR_HOOKS_TMUX="Tmux Reminder - Suggest run_in_background for foreground dev servers (non-blocking)"
STR_HOOKS_GIT_PUSH="Git Push Review - Pause before git push for review"
STR_HOOKS_DOC_BLOCK="Doc Blocker - Block creation of random .md/.txt files"
STR_HOOKS_DOC_BLOCK="Doc Blocker - Confirm ad-hoc SUMMARY/REPORT style docs (general docs pass)"
STR_HOOKS_PRETTIER="Prettier Auto-format - Format JS/TS files after edits"
STR_HOOKS_BIOME="Biome Auto-format - Format and lint JS/TS files after edits"
STR_HOOKS_CONSOLE="Console.log Guard - Warn about console.log statements"
Expand Down
2 changes: 1 addition & 1 deletion i18n/ja/strings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ STR_HOOKS_AUTO_UPDATE="自動アップデート - セッション開始時にス
STR_HOOKS_WEB_CONTENT_UPDATE="Web取得スキル更新 - セッション開始時に web-content-extraction の依存を自動更新(opt-in)"
STR_HOOKS_TMUX="Tmux リマインダー - foreground の dev サーバーに run_in_background を提案(非ブロック)"
STR_HOOKS_GIT_PUSH="Git Push レビュー - push 前に一時停止してレビュー"
STR_HOOKS_DOC_BLOCK="ドキュメントブロッカー - 不要な .md/.txt ファイルの作成を防止"
STR_HOOKS_DOC_BLOCK="Doc ブロッカー - SUMMARY/REPORT 等のアドホック文書のみ確認(一般文書は素通し)"
STR_HOOKS_PRETTIER="Prettier 自動フォーマット - JS/TS ファイルを編集後にフォーマット"
STR_HOOKS_BIOME="Biome 自動フォーマット - JS/TS ファイルを編集後に整形と lint 修正"
STR_HOOKS_CONSOLE="Console.log ガード - console.log の警告"
Expand Down
74 changes: 56 additions & 18 deletions tests/unit/test-doc-blocker.sh
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
#!/bin/bash
# tests/unit/test-doc-blocker.sh - doc-blocker path allowlist tests
# tests/unit/test-doc-blocker.sh - deny-by-pattern doc guard tests

_doc_blocker_script="$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh"

_run_doc_blocker_path() {
local path="$1"
printf '{"tool_input":{"file_path":"%s"}}' "$path" | "$_doc_blocker_script" >/dev/null
printf '{"tool_input":{"file_path":"%s"}}' "$path" | "$_doc_blocker_script"
}

_doc_blocker_decision() {
local path="$1"
_run_doc_blocker_path "$path" | jq -r '.hookSpecificOutput.permissionDecision // "allow"' 2>/dev/null || printf 'allow'
}

{
test_name="doc-blocker: allows Starter Kit command and agent output paths"
test_name="doc-blocker: kit output paths stay allowed (even slop-like names)"
allowed=(
"$PROJECT_DIR/HANDOVER.md"
"$PROJECT_DIR/research.md"
"$PROJECT_DIR/notes/research.md"
"$PROJECT_DIR/sessions/HANDOVER.md"
"$PROJECT_DIR/docs/CONTRIB.md"
"$PROJECT_DIR/docs/RUNBOOK.md"
"$PROJECT_DIR/docs/CODEMAPS/frontend.md"
"$PROJECT_DIR/docs/CODEMAPS/architecture.md"
"$PROJECT_DIR/codemaps/backend.md"
"$PROJECT_DIR/docs/GUIDES/setup.md"
"$PROJECT_DIR/docs/DELETION_LOG.md"
"$PROJECT_DIR/.reports/dead-code-analysis.md"
"$PROJECT_DIR/.reports/codemap-diff.txt"
"$PROJECT_DIR/.reports/SUMMARY.md"
"$PROJECT_DIR/docs/CODEMAPS/ANALYSIS.md"
"$PROJECT_DIR/.claude/evals/feature-name.md"
"$HOME/.claude/skills/learned/pattern.md"
"$PROJECT_DIR/.specify/memory/constitution.md"
)
ok_all=true
for path in "${allowed[@]}"; do
_run_doc_blocker_path "$path" || ok_all=false
[[ "$(_doc_blocker_decision "$path")" == "allow" ]] || ok_all=false
done

if [[ "$ok_all" == "true" ]]; then
pass "$test_name"
else
Expand All @@ -41,23 +43,59 @@ _run_doc_blocker_path() {
}

{
test_name="doc-blocker: still blocks ad-hoc markdown and text files"
if ! _run_doc_blocker_path "$PROJECT_DIR/random-notes.md" \
&& ! _run_doc_blocker_path "$PROJECT_DIR/tmp/scratch.txt"; then
test_name="doc-blocker: general docs pass without confirmation (inverted from allowlist)"
general=(
"$PROJECT_DIR/CHANGELOG.md"
"$PROJECT_DIR/LICENSE.txt"
"$PROJECT_DIR/docs/architecture.md"
"$PROJECT_DIR/docs/adr/0001-record.md"
"$PROJECT_DIR/release-notes.md"
"$PROJECT_DIR/market-research.md"
)
ok_all=true
for path in "${general[@]}"; do
[[ "$(_doc_blocker_decision "$path")" == "allow" ]] || ok_all=false
done
if [[ "$ok_all" == "true" ]]; then
pass "$test_name"
else
fail "$test_name"
fi
}

{
test_name="doc-blocker: rejects suffix variants of research.md / HANDOVER.md with exit 2"
rc_research=0
_run_doc_blocker_path "$PROJECT_DIR/market-research.md" 2>/dev/null || rc_research=$?
rc_handover=0
_run_doc_blocker_path "$PROJECT_DIR/PROJECT-HANDOVER.md" 2>/dev/null || rc_handover=$?
test_name="doc-blocker: slop doc names are downgraded to a permission ask (exit 0)"
slop=(
"$PROJECT_DIR/SUMMARY.md"
"$PROJECT_DIR/FINAL_REPORT.md"
"$PROJECT_DIR/analysis.md"
"$PROJECT_DIR/sub/dir/FINDINGS.md"
"$PROJECT_DIR/NOTES_2026.txt"
"$PROJECT_DIR/results.md"
)
ok_all=true
for path in "${slop[@]}"; do
rc=0
out="$(_run_doc_blocker_path "$path")" || rc=$?
[[ "$rc" -eq 0 ]] || ok_all=false
[[ "$(printf '%s' "$out" | jq -r '.hookSpecificOutput.permissionDecision // empty')" == "ask" ]] || ok_all=false
printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecisionReason | contains("doc-blocker")' >/dev/null || ok_all=false
done
if [[ "$ok_all" == "true" ]]; then
pass "$test_name"
else
fail "$test_name"
fi
}

if [[ "$rc_research" -eq 2 && "$rc_handover" -eq 2 ]]; then
{
test_name="doc-blocker: non-doc files pass and hard blocks are gone (no exit 2)"
rc_code=0
_run_doc_blocker_path "$PROJECT_DIR/src/main.ts" >/dev/null || rc_code=$?
rc_slop=0
_run_doc_blocker_path "$PROJECT_DIR/SUMMARY.md" >/dev/null || rc_slop=$?
if [[ "$rc_code" -eq 0 && "$rc_slop" -eq 0 ]] \
&& ! grep -q 'exit 2' "$_doc_blocker_script"; then
pass "$test_name"
else
fail "$test_name"
Expand Down
12 changes: 6 additions & 6 deletions tests/unit/test-hook-fixtures.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ else
fi

_doc_fixture="$_hook_fixture_dir/pretooluse-write-doc.json"
_doc_err="$_hook_tmp/doc.err"
_doc_out="$_hook_tmp/doc.out"
_doc_rc=0
bash "$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh" <"$_doc_fixture" >/dev/null 2>"$_doc_err" || _doc_rc=$?
bash "$PROJECT_DIR/features/doc-blocker/scripts/check-doc-write.sh" <"$_doc_fixture" >"$_doc_out" 2>/dev/null || _doc_rc=$?

if [[ "$_doc_rc" -eq 2 ]] \
&& assert_matches "\\[Hook\\] BLOCKED: Use README.md" "$(cat "$_doc_err")"; then
pass "hook-fixtures: doc-blocker consumes PreToolUse Write fixture"
if [[ "$_doc_rc" -eq 0 ]] \
&& [[ "$(jq -r '.hookSpecificOutput.permissionDecision // empty' "$_doc_out")" == "ask" ]]; then
pass "hook-fixtures: doc-blocker downgrades slop doc Write to a permission ask"
else
fail "hook-fixtures: doc-blocker should block ad-hoc docs from fixture input"
fail "hook-fixtures: doc-blocker should emit an ask decision for slop docs"
fi

_post_edit_fixture="$_hook_tmp/post-edit.json"
Expand Down
Loading