IBM BobのLifecycle HooksでAIにルールを遵守させる
本記事はIBM Bobを活用して作成されています。
はじめに
IBM Bobに限らず一般のコーディングエージェントを使う際、例えばこんな課題に直面したことはありませんか?
「ESLintのルールを必ず守らせる方法はないだろうか。AGENTS.mdの中にルールを設定してみたのですが、上手くいかないんです。」
AGENTS.mdやルールやモードが「上手くいかない」のは、設定ミスだけが原因ではありません。 rulesという仕組みそのものが持つ性質を理解しないと、どれだけ丁寧にルールを書いても「必ず守らせる」ことはできません。
この記事ではIBM Bob 2.0.2で追加されたLifecycle Hooks機能を用いて、作業ルール違反を検出して差し戻すハーネスを実装する例を以下の3ステップで解説します。
https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks
- なぜ
.bob/rulesだけでは"必ず守らせる"ことができないのか - IBM Bobが持つLifecycle Hooksという仕組みとは何か
- 5つのHookを組み合わせた決定論的ルール検査ハーネスの実装例
なぜ .bob/rules だけでは"必ず守らせる"ことができないのか
rulesはLLMへの「お願い」である
.bob/rules/ に配置したMarkdownファイルの内容は、IBM Bobが毎回の会話コンテキストにプロンプトとして注入されます。つまり、rulesはLLMに対する「指示」であり、LLMが「そのように振る舞おうとする」ことを期待するものです。
これは本質的に確率的です。LLMはルールを読んだ上で動作しますが、「必ずそのルールに従う」という保証はシステムレベルでは存在しません。
コンテキスト汚染による信頼性の低下
設定が正しくても、長いセッションではrulesの効果が低下します。これはコンテキスト汚染と呼ばれる現象です。会話履歴が積み重なり、ツール出力や中間結果がコンテキストを埋め尽くすと、rulesの内容がLLMの注意から外れていきます。
IBM Bobの公式ドキュメントは明確に述べています。
「信頼できる修正方法は新しいタスクを開始することです。チャットパネルの +(New Task) をクリックしてください。」
ソフト強制 vs ハード強制
| 比較項目 |
.bob/rules(ソフト強制) |
Lifecycle Hooks(ハード強制) |
|---|---|---|
| 実行保証 | LLMが「守ろうとする」 | プログラムが必ず実行する |
| コンテキスト依存 | あり(長いセッションで低下) | なし |
| ブロッキング | できない |
exit 2 で物理的にブロック |
| 対象 | LLMの行動傾向 | ツールの実行・プロンプトの送信 |
| 設定場所 | .bob/rules/*.md |
settings.json の hooks キー |
rulesは「こう振る舞ってほしい」というガイドラインであり、Lifecycle Hooksは「これ以外は物理的に実行できない」というゲートです。 厳密な品質管理には後者が必要です。
IBM Bobのライフサイクルフックとは
IBM Bobには、セッションの重要なタイミングでシェルコマンドを自動実行するLifecycle Hooks機能があります。設定は settings.json の hooks キーで行います。
5つのHookと特性
| Hook | 実行タイミング | ブロッキング | stdoutの動作 |
|---|---|---|---|
SessionStart |
セッション開始時に1回 | なし | コンテキストとして注入 |
UserPromptSubmit |
プロンプト送信のたびに | exit 2 でブロック |
コンテキストとして注入 |
PreToolUse |
対象ツール実行前 | exit 2 でブロック |
無視 |
PostToolUse |
対象ツール完了後 | なし | 無視 |
Stop |
エージェント停止時 | なし | 無視 |
ブロッキングをサポートするのは UserPromptSubmit と PreToolUse のみです。SessionStart、PostToolUse、Stop から exit 2 を返しても、ブロッキングは発生しません。
設定場所
グローバル(全workspace共通): ~/.bob/settings/settings.json
Workspace(現在のプロジェクトのみ): .bob/settings.json
グローバルHooksは常に実行されます。WorkspaceのHooksはグローバルHooksに上書きされるのではなく、追加で実行されます。
設定スキーマ
{
"hooks": {
"PreToolUse": [
{
"matcher": "^write_file$",
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/pre-tool-guard.sh",
"timeout": 5
}
]
}
]
}
}
| フィールド | 型 | 説明 |
|---|---|---|
type |
"command" |
必須。現在は command のみサポート |
command |
string | 必須。実行するシェルコマンド(macOS/Linuxは sh -c、Windowsは cmd /c) |
matcher |
string | 任意。ツール名に対するregex(PreToolUse、PostToolUse のみ有効) |
timeout |
number | デフォルト10秒。0 でタイムアウト無効 |
exit 2によるブロッキングの仕組み
Hookのシェルスクリプトが exit 2 を返すと、IBM Bobは現在のアクションをプログラムレベルで停止します。
-
UserPromptSubmitでexit 2→ プロンプトがLLMに送信されない -
PreToolUseでexit 2→ ツールが実行されない(LLMが「やろうとしていても」物理的に不可能)
これが「決定論的検査」の核心です。LLMの判断を介さず、シェルスクリプトのロジックが直接制御します。
決定論的ルール検査ハーネスの設計例
全体アーキテクチャ
┌─────────────────────────────────────────────────────────┐
│ Bob セッション │
│ │
│ [開始]──→ SessionStart ──→ Linter設定の自動検出・注入 │
│ │ │
│ ↓ │
│ [プロンプト]→ UserPromptSubmit ──→ 危険ワード検査 │
│ │ exit 2 → ブロック │
│ ↓ │
│ [ツール実行前]→ PreToolUse ──→ Linter実行 + 固定長検査 │
│ │ exit 2 → ブロック ★コア │
│ ↓ │
│ [ツール完了後]→ PostToolUse ──→ 監査ログ記録 │
│ │ │
│ ↓ │
│ [終了]──→ Stop ──→ 品質レポート自動生成 │
└─────────────────────────────────────────────────────────┘
ディレクトリ構成
.bob/
├── settings.json # Hook登録
└── hooks/
├── session-start.sh # SessionStart: Linter設定の自動検出
├── prompt-guard.sh # UserPromptSubmit: 危険プロンプトブロック
├── pre-tool-guard.sh # PreToolUse: 決定論的ツール検査 ★コア
├── code-policy-check.sh # PreToolUse から呼ばれるLinter統合チェック
├── audit-log.sh # PostToolUse: 監査ログ
└── stop-report.sh # Stop: セッション終了レポート
実装: 5つのHookでハーネスを組む
settings.json の全体設定
まず全Hookを一括で登録します。
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/session-start.sh"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/prompt-guard.sh",
"timeout": 5
}
]
}
],
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/pre-tool-guard.sh",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/audit-log.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "sh .bob/hooks/stop-report.sh"
}
]
}
]
}
}
Step 1: SessionStart — プロジェクト品質基準の自動注入
セッション開始時に1回だけ実行されます。stdoutに出力した内容がLLMのコンテキストに追加されます。これを利用して、プロジェクトのコーディング規約やLinter設定をBobに自動的に伝えます。
#!/bin/sh
# .bob/hooks/session-start.sh
# プロジェクトの品質基準とLinter設定をコンテキストに注入
echo "=== コード品質ゲート有効化 ==="
echo "プロジェクト: $(basename $PWD)"
echo "Gitブランチ: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'N/A')"
echo ""
echo "【有効なLinter】"
# 利用可能なLinterを検出して表示
if command -v eslint >/dev/null 2>&1; then
echo "✓ ESLint (JavaScript/TypeScript)"
[ -f ".eslintrc.json" ] && echo " 設定: .eslintrc.json"
fi
if command -v pylint >/dev/null 2>&1; then
echo "✓ Pylint (Python)"
[ -f "pylintrc" ] && echo " 設定: pylintrc"
fi
if command -v checkstyle >/dev/null 2>&1; then
echo "✓ Checkstyle (Java)"
[ -f "checkstyle.xml" ] && echo " 設定: checkstyle.xml"
fi
echo ""
echo "【重要: このセッションのルール】"
echo "- 全てのファイル書き込みは対応するLinterで自動検査されます"
echo "- Linterエラーがある場合、書き込みは物理的にブロックされます"
echo "- COBOL固定長フォーマット(80桁、カラム7制約)は厳密に検査されます"
echo "- デバッグ出力、TODO、機密情報のハードコードは禁止です"
echo ""
echo "上記ルールはシステムレベルで強制されています。"
このスクリプトのポイントは、利用可能なLinterを自動検出してLLMに伝えることです。LLMはこの情報を踏まえて、各言語の規約に準拠したコードを生成しようとします(rulesと組み合わせるソフト強制の役割)。
Step 2: UserPromptSubmit — 品質基準を下げる指示のブロック
プロンプトがLLMに送信される前に毎回実行されます。exit 2 を返すとプロンプトの送信がブロックされます。
#!/bin/sh
# .bob/hooks/prompt-guard.sh
# 品質基準を下げる指示や危険なキーワードを含むプロンプトをブロック
# stdinからJSONペイロードを受け取り、promptフィールドを取得
PROMPT=$(cat | jq -r '.prompt')
# Linterを無視する指示を検出
case "$PROMPT" in
*"eslint"*"無視"*|*"ignore eslint"*|*"skip linter"*)
echo "ブロック: Linterを無視する指示は許可されていません" >&2
exit 2
;;
*"TODO"*"残して"*|*"leave TODO"*)
echo "ブロック: TODOコメントを残す指示は許可されていません" >&2
exit 2
;;
*"console.log"*"残して"*|*"keep console.log"*)
echo "ブロック: デバッグ出力を残す指示は許可されていません" >&2
exit 2
;;
*"本番"*|*"production"*|*"prod"*)
echo "ブロック: 本番環境への操作はこのセッションでは許可されていません" >&2
exit 2
;;
esac
exit 0
jq で stdin のJSONから prompt フィールドを取り出すのがポイントです。BobはHookの stdin に以下のようなJSONを渡します。
{
"event": "UserPromptSubmit",
"session_id": "ses_01abc123",
"prompt": "ESLintエラーを無視してファイルを作成してください"
}
このHookにより、ユーザーが「Linterを無視して」「TODOを残して」といった品質基準を下げる指示を出しても、プロンプト自体がLLMに届かないため、IBM Bobは応答できません。
Step 3: PreToolUse — ツールレベルの決定論的検査(★ 最重要)
このHookがハーネスの心臓部です。
ツールが実行される直前に毎回呼ばれます。exit 2 を返すとツールの実行が物理的にブロックされます。LLMが「やろうとしていても」実行できません。
BobはHookの stdin に以下のようなJSONを渡します。
{
"event": "PreToolUse",
"session_id": "ses_01abc123",
"tool_name": "write_file",
"tool_input": {
"path": "legacy-core/billing/processor.java",
"content": "..."
}
}
このJSONを jq で解析して検査します。ハーネスは 保護領域チェック と コーディング規約チェック の2段構えになっています。
pre-tool-guard.sh — 検査の司令塔
#!/bin/sh
# .bob/hooks/pre-tool-guard.sh
# ツール実行前の決定論的検査
HOOK_DIR="$(dirname "$0")"
# stdinを一時ファイルに保存
# 【重要】INPUT=$(cat) のようにシェル変数に入れると改行を含む
# JSONのparse errorが発生する。mktemp 経由が安全。
TMPINPUT=$(mktemp)
cat > "$TMPINPUT"
TOOL=$(jq -r '.tool_name' "$TMPINPUT")
case "$TOOL" in
# ファイル書き込み系ツールの保護領域チェック + コーディング規約チェック
write_file|apply_diff|insert_content|search_and_replace)
PATH_VAL=$(jq -r '.tool_input.path // empty' "$TMPINPUT")
# 1. 保護領域チェック(先に判定)
case "$PATH_VAL" in
legacy-core/*|db/schema/*)
echo "ブロック: '$PATH_VAL' は保護領域です。書き込みは許可されていません。" >&2
rm -f "$TMPINPUT"; exit 2
;;
esac
# 2. コーディング規約チェック(write_file のみ content がある)
if [ "$TOOL" = "write_file" ]; then
TMPCONTENT=$(mktemp)
jq -r '.input.content // empty' "$TMPINPUT" > "$TMPCONTENT"
if [ -s "$TMPCONTENT" ]; then
sh "$HOOK_DIR/code-policy-check.sh" "$PATH_VAL" < "$TMPCONTENT"
CHECK_EXIT=$?
rm -f "$TMPCONTENT" "$TMPINPUT"
[ $CHECK_EXIT -ne 0 ] && exit $CHECK_EXIT
else
rm -f "$TMPCONTENT"
fi
fi
;;
# コマンド実行の危険コマンドチェック
execute_command)
CMD=$(jq -r '.tool_input.command // empty' "$TMPINPUT") 
case "$CMD" in
*"rm -rf"*|*"rm -r"*)
echo "ブロック: 再帰削除コマンドは許可されていません: $CMD" >&2
rm -f "$TMPINPUT"; exit 2
;;
*"git push"*)
echo "ブロック: git push はこのセッションで許可されていません" >&2
rm -f "$TMPINPUT"; exit 2
;;
*"git reset --hard"*)
echo "ブロック: git reset --hard は許可されていません" >&2
rm -f "$TMPINPUT"; exit 2
;;
esac
;;
esac
rm -f "$TMPINPUT"
exit 0
INPUT=$(cat) ではなく mktemp を使う理由
INPUT=$(cat) でシェル変数に入れると、後で echo "$INPUT" | jq ... としたときに変数展開が改行を壊し、jqがparse errorを返します。mktemp で一時ファイルに保存して jq -r '.field' "$TMPFILE" と読めば、複数回パースしても安全です。
code-policy-check.sh — Linter統合とフォーマット検査
write_file の content を受け取り、実際のLinterツール(ESLint、Pylint、Checkstyle等)を実行して規約違反があれば exit 2 でブロックします。さらに、COBOL固定長フォーマット(カラム7-72制約)のような言語特有の制約の自動検査にも応用できます。
#!/bin/sh
# .bob/hooks/code-policy-check.sh
# write_file 直前にコンテンツをlinterでチェックし、
# 規約違反があれば exit 2 でブロックする
#
# stdin : 書き込もうとしているファイルの内容
# $1 : 対象ファイルのパス
FILE_PATH="${1:-}"
VIOLATIONS=""
# stdin をファイルに保存して複数回チェックできるようにする
TMPCHK=$(mktemp)
cat > "$TMPCHK"
add_violation() {
MESSAGE="$1"
DETAILS="$2"
VIOLATIONS="${VIOLATIONS} ✗ ${MESSAGE}\n${DETAILS}\n"
}
# ファイル拡張子を取得
EXT="${FILE_PATH##*.}"
# ── 言語別Linterチェック ──────────────────────────────────
case "$EXT" in
js|jsx|ts|tsx)
# ESLint統合チェック
if command -v eslint >/dev/null 2>&1; then
ESLINT_OUTPUT=$(eslint --no-eslintrc --config .eslintrc.json \
--stdin --stdin-filename "$FILE_PATH" < "$TMPCHK" 2>&1 || true)
if echo "$ESLINT_OUTPUT" | grep -q "error"; then
ERRORS=$(echo "$ESLINT_OUTPUT" | grep "error" | head -5 | sed 's/^/ /')
add_violation "ESLintエラーが検出されました" "$ERRORS"
fi
fi
;;
py)
# Pylint統合チェック
if command -v pylint >/dev/null 2>&1; then
PYLINT_OUTPUT=$(pylint --from-stdin "$FILE_PATH" < "$TMPCHK" 2>&1 || true)
if echo "$PYLINT_OUTPUT" | grep -qE "E[0-9]{4}|F[0-9]{4}"; then
ERRORS=$(echo "$PYLINT_OUTPUT" | grep -E "E[0-9]{4}|F[0-9]{4}" | head -5 | sed 's/^/ /')
add_violation "Pylintエラーが検出されました" "$ERRORS"
fi
fi
;;
java)
# Checkstyle統合チェック
if command -v checkstyle >/dev/null 2>&1 && [ -f "checkstyle.xml" ]; then
TMPJAVA=$(mktemp --suffix=.java)
cp "$TMPCHK" "$TMPJAVA"
CHECKSTYLE_OUTPUT=$(checkstyle -c checkstyle.xml "$TMPJAVA" 2>&1 || true)
rm -f "$TMPJAVA"
if echo "$CHECKSTYLE_OUTPUT" | grep -q "ERROR"; then
ERRORS=$(echo "$CHECKSTYLE_OUTPUT" | grep "ERROR" | head -5 | sed 's/^/ /')
add_violation "Checkstyleエラーが検出されました" "$ERRORS"
fi
fi
;;
cbl|cob|cobol)
# COBOL固定長フォーマットチェック(カラム7-72の制約)
LINE_NUM=0
while IFS= read -r line; do
LINE_NUM=$((LINE_NUM + 1))
LINE_LEN=${#line}
# カラム1-6: シーケンス番号エリア(オプション)
# カラム7: インジケータエリア(*, /, -, D, 空白のみ)
# カラム8-72: Aエリア・Bエリア(コード本体)
# カラム73-80: 識別エリア(オプション)
if [ $LINE_LEN -gt 80 ]; then
add_violation "COBOL固定長違反: 行${LINE_NUM}が80桁を超えています(${LINE_LEN}桁)" \
" 行${LINE_NUM}: $(echo "$line" | cut -c1-80)..."
fi
# カラム7のインジケータチェック
if [ $LINE_LEN -ge 7 ]; then
INDICATOR=$(echo "$line" | cut -c7)
case "$INDICATOR" in
" "|"*"|"/"|"-"|"D") ;;
*)
add_violation "COBOL固定長違反: 行${LINE_NUM}のカラム7に不正な文字 '$INDICATOR'" \
" 行${LINE_NUM}: $(echo "$line" | cut -c1-72)"
;;
esac
fi
done < "$TMPCHK"
;;
esac
# ── 全言語共通チェック ──────────────────────────────────
# デバッグ出力の残し
if grep -qE 'console\.log\(|print[[:space:]]*\(|System\.out\.print|fmt\.Print\(|debugger;' "$TMPCHK" 2>/dev/null; then
MATCHES=$(grep -nE 'console\.log\(|print[[:space:]]*\(|System\.out\.print|fmt\.Print\(|debugger;' "$TMPCHK" | head -3 | sed 's/^/ /')
add_violation "デバッグ出力が残っています" "$MATCHES"
fi
# TODOコメントの残し
if grep -qE 'TODO:|FIXME:|HACK:|XXX:' "$TMPCHK" 2>/dev/null; then
MATCHES=$(grep -nE 'TODO:|FIXME:|HACK:|XXX:' "$TMPCHK" | head -3 | sed 's/^/ /')
add_violation "TODO/FIXME/HACK/XXXコメントが残っています" "$MATCHES"
fi
# ハードコードされた機密情報
if grep -qE "(password|passwd|secret|api_key|apikey|token)[[:space:]]*[=:][[:space:]]*['\"][^'\"]{4,}" "$TMPCHK" 2>/dev/null; then
MATCHES=$(grep -nE "(password|passwd|secret|api_key|apikey|token)[[:space:]]*[=:][[:space:]]*['\"][^'\"]{4,}" "$TMPCHK" | head -3 | sed 's/^/ /')
add_violation "機密情報がハードコードされている可能性があります" "$MATCHES"
fi
# APIキーパターン
if grep -qE '(AKIA|sk-|ghp_|gho_|ghu_)[A-Za-z0-9]{10,}' "$TMPCHK" 2>/dev/null; then
MATCHES=$(grep -nE '(AKIA|sk-|ghp_|gho_|ghu_)[A-Za-z0-9]{10,}' "$TMPCHK" | head -3 | sed 's/^/ /')
add_violation "APIキーらしき文字列が含まれています" "$MATCHES"
fi
# IPアドレスのハードコード
if grep -qE 'https?://[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' "$TMPCHK" 2>/dev/null; then
MATCHES=$(grep -nE 'https?://[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*' "$TMPCHK" | head -3 | sed 's/^/ /')
add_violation "IPアドレスがURLにハードコードされています" "$MATCHES"
fi
# ライセンスヘッダーチェック(企業コードの場合)
if [ -f ".license-header" ]; then
HEADER_LINES=$(wc -l < ".license-header")
FILE_HEADER=$(head -n "$HEADER_LINES" "$TMPCHK")
EXPECTED_HEADER=$(cat ".license-header")
if [ "$FILE_HEADER" != "$EXPECTED_HEADER" ]; then
add_violation "必須ライセンスヘッダーが含まれていません" \
" 期待: $(head -1 .license-header)\n 実際: $(head -1 "$TMPCHK")"
fi
fi
# ── 結果判定 ────────────────────────────────────────────
rm -f "$TMPCHK"
if [ -n "$VIOLATIONS" ]; then
echo "コーディング規約違反を検出しました: $FILE_PATH" >&2
printf "%b" "$VIOLATIONS" >&2
echo "" >&2
echo "上記を修正してから再度書き込みを試みてください。" >&2
exit 2
fi
exit 0
実用的なユースケース例:
-
JavaScript/TypeScript開発: ESLintの
no-console、no-debuggerルールを強制 -
Python開発: Pylintで
E0602(未定義変数)、F0401(import失敗)を検出 - Java開発: Checkstyleで命名規則、インデント、Javadoc必須を強制
- COBOL保守: 固定長フォーマット(80桁制限、カラム7インジケータ)を厳密に検査
-
企業コード:
.license-headerファイルで定義したライセンスヘッダーの存在を強制
ブロック時のメッセージ例(ESLint統合):
コーディング規約違反を検出しました: src/service.js
✗ ESLintエラーが検出されました
3:5 error Unexpected console statement no-console
7:3 error 'userId' is not defined no-undef
12:1 error Unexpected debugger statement no-debugger
上記を修正してから再度書き込みを試みてください。
ブロック時のメッセージ例(COBOL固定長違反):
コーディング規約違反を検出しました: legacy/billing.cbl
✗ COBOL固定長違反: 行42が80桁を超えています(85桁)
行42: MOVE CUSTOMER-ID TO WS-CUSTOMER-ID-BUFFER-EXTENDED-FIELD...
✗ COBOL固定長違反: 行58のカラム7に不正な文字 'X'
行58: XPERFORM CALCULATE-TOTAL
上記を修正してから再度書き込みを試みてください。
なぜ write_file だけでなく apply_diff、insert_content、search_and_replace もチェックするのか
IBM Bobはファイル書き込みに複数のツールを使います。write_file だけを制限しても、apply_diff で同じファイルを変更できてしまいます。保護したいパスに対するすべての書き込み系ツールを網羅的にチェックすることが重要です。ただしコーディング規約チェック(content検査)は write_file のみが content を持つため、そちらだけに適用します。
Step 4: PostToolUse — 解析操作の監査ログ
ツール完了後に実行されます。ブロッキングはできませんが、全ての操作をログに記録することでセッション終了後に何を行ったかを追跡できます。
#!/bin/sh
# .bob/hooks/audit-log.sh
# 全ツール操作をJSONL形式で監査ログに記録
LOG_DIR=".bob/audit-logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$(date -u +%Y-%m-%d).jsonl"
# stdinのJSONにタイムスタンプを追加してJSONLに追記
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cat | jq --arg ts "$TIMESTAMP" '. + {logged_at: $ts}' >> "$LOG_FILE"
Step 5: Stop — 品質セッション終了レポートの自動生成
エージェントが停止したときに実行されます。監査ログを集計して品質レポートを生成します。
#!/bin/sh
# .bob/hooks/stop-report.sh
# セッション終了時に品質レポートを自動生成
LOG_DIR=".bob/audit-logs"
REPORT_DIR="quality-report"
mkdir -p "$REPORT_DIR"
TODAY=$(date -u +%Y-%m-%d)
REPORT_FILE="$REPORT_DIR/session-report-$TODAY.md"
LOG_FILE="$LOG_DIR/$TODAY.jsonl"
{
echo "# コード品質セッション レポート"
echo "**日時**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
echo "**プロジェクト**: $(basename $PWD)"
echo ""
echo "## 実行ツール集計"
echo ""
if [ -f "$LOG_FILE" ]; then
echo "| ツール | 実行回数 |"
echo "|--------|---------|"
jq -r '.tool' "$LOG_FILE" | sort | uniq -c | sort -rn | \
awk '{print "| " $2 " | " $1 " |"}'
echo ""
echo "## 書き込み操作一覧"
echo ""
jq -r 'select(.tool == "write_file" or .tool == "apply_diff") |
"- `" + (.input.path // "N/A") + "` (" + .logged_at + ")"' "$LOG_FILE" 2>/dev/null || \
echo "書き込み操作はありませんでした。"
echo ""
echo "## 品質ゲート統計"
echo ""
# ブロックされた操作をカウント(実際の実装ではPreToolUseのログから取得)
BLOCKED_COUNT=$(grep -c "exit 2" .bob/hooks/*.sh 2>/dev/null || echo "0")
echo "- **ブロックされた操作**: ${BLOCKED_COUNT}件"
# 言語別のファイル数を集計
echo ""
echo "### 作成・変更されたファイル(言語別)"
echo ""
jq -r 'select(.tool == "write_file") | .input.path' "$LOG_FILE" 2>/dev/null | \
sed 's/.*\.//' | sort | uniq -c | sort -rn | \
awk '{print "- **" $2 "**: " $1 "ファイル"}'
else
echo "ログファイルが見つかりません: $LOG_FILE"
fi
} > "$REPORT_FILE"
echo "品質レポートを生成しました: $REPORT_FILE"
生成されるレポート例:
# コード品質セッション レポート
**日時**: 2026-08-21 05:47:00 UTC
**プロジェクト**: my-project
## 実行ツール集計
| ツール | 実行回数 |
|--------|---------|
| write_file | 15 |
| read_file | 42 |
| search_file_content | 8 |
| apply_diff | 3 |
## 書き込み操作一覧
- `src/components/Button.tsx` (2026-08-21T05:30:15Z)
- `src/utils/validator.ts` (2026-08-21T05:35:22Z)
- `tests/validator.test.ts` (2026-08-21T05:40:10Z)
## 品質ゲート統計
- **ブロックされた操作**: 2件
### 作成・変更されたファイル(言語別)
- **tsx**: 8ファイル
- **ts**: 5ファイル
- **json**: 2ファイル
動作確認とトラブルシュート
確認方法: Linter統合が正しく動作しているか
Step 1: テスト用の違反コードを書き込んでブロックされることを確認します。
// test-violation.js
console.log("This should be blocked"); // ESLintのno-consoleルール違反
const apiKey = "sk-1234567890"; // 機密情報のハードコード
Bobに「test-violation.js を作成してください」と指示すると、以下のようにブロックされます:
* test-cobol-violation.cbl
XIDENTIFICATION DIVISION.
PROGRAM-ID. TEST-PROGRAM-WITH-VERY-LONG-NAME-THAT-EXCEEDS-EIGHTY-CHARACTERS.
このコードは以下の理由でブロックされます:
- 行2: カラム7に不正な文字 'X'
- 行3: 80桁を超える行長
そして、ブロックされたことをIBM Bobが認識して修正作業に移ってくれます。
よくあるトラブル
| 症状 | 原因 | 対処 |
|---|---|---|
| Hookが実行されない |
settings.json の場所が違う |
~/.bob/settings/settings.json または .bob/settings.json を確認 |
| Linterエラーが検出されない | Linterがインストールされていない |
npm install -g eslint(ESLint)/ pip install pylint(Pylint)等 |
| Linter設定ファイルが見つからない |
.eslintrc.json 等が存在しない |
プロジェクトルートに設定ファイルを配置 |
exit 2 でブロックされない |
ブロッキング非対応のHookを使っている |
SessionStart / PostToolUse / Stop はブロック不可。PreToolUse を使う |
| jqコマンドが見つからない |
jq が未インストール |
brew install jq(macOS)/ apt install jq(Ubuntu) |
| Hookがタイムアウトする | Linterの処理が重い |
"timeout" の値を増やす(デフォルト10秒、Linterは15-30秒推奨) |
| COBOL固定長チェックが動かない | ファイル拡張子が認識されない |
.cbl、.cob、.cobol のいずれかを使用 |
まとめ
rules vs Lifecycle Hooks の使い分け
| 目的 | 使うべき機能 |
|---|---|
| コーディングスタイルの統一 | .bob/rules/ |
| Linterエラーを必ずブロックする |
PreToolUse Hook + Linter統合 + exit 2
|
| 固定長フォーマットを必ず検査する |
PreToolUse Hook + カスタムチェック + exit 2
|
| 危険なプロンプトを必ずブロックする |
UserPromptSubmit Hook + exit 2
|
| セッション開始時にLinter設定を注入する |
SessionStart Hook |
| 全操作の監査ログを取る |
PostToolUse Hook |
| セッション終了後に品質レポートを生成する |
Stop Hook |
Linter統合品質ゲートの設計4原則
-
既存のLinterツールをそのまま活用する — ESLint、Pylint、Checkstyle等の標準ツールを
command -vで検出し、プロジェクトの設定ファイル(.eslintrc.json、pylintrc、checkstyle.xml)をそのまま使う - 言語別チェックと全言語共通チェックを分離する — 拡張子で言語を判定してLinterを切り替え、デバッグ出力・機密情報等の共通チェックは全言語に適用
-
stdin は
mktempで一時ファイルに保存する —INPUT=$(cat)でシェル変数に入れると改行を含むJSONでjqがparse errorを返す。mktemp経由でjq -r '.field' "$TMPFILE"と読めば安全 -
PostToolUseとStopでブロックしようとしない — この2つは
exit 2が効かない。事後処理(ログ・レポート)専用として使う
実用的な適用例
- JavaScript/TypeScript: ESLintで
no-console、no-debugger、no-unused-varsを強制 - Python: Pylintで
E0602(未定義変数)、W0611(未使用import)を検出 - Java: Checkstyleで命名規則、インデント、Javadoc必須を強制
- COBOL: 固定長フォーマット(80桁制限、カラム7インジケータ)を厳密に検査
- 企業コード:
.license-headerファイルで定義したライセンスヘッダーの存在を強制

