3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

IBM BobのLifecycle HooksでAIにルールを遵守させる

3
Last updated at Posted at 2026-08-21

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

  1. なぜ .bob/rules だけでは"必ず守らせる"ことができないのか
  2. IBM Bobが持つLifecycle Hooksという仕組みとは何か
  3. 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.jsonhooks キー

rulesは「こう振る舞ってほしい」というガイドラインであり、Lifecycle Hooksは「これ以外は物理的に実行できない」というゲートです。 厳密な品質管理には後者が必要です。


IBM Bobのライフサイクルフックとは

IBM Bobには、セッションの重要なタイミングでシェルコマンドを自動実行するLifecycle Hooks機能があります。設定は settings.jsonhooks キーで行います。

5つのHookと特性

Hook 実行タイミング ブロッキング stdoutの動作
SessionStart セッション開始時に1回 なし コンテキストとして注入
UserPromptSubmit プロンプト送信のたびに exit 2 でブロック コンテキストとして注入
PreToolUse 対象ツール実行前 exit 2 でブロック 無視
PostToolUse 対象ツール完了後 なし 無視
Stop エージェント停止時 なし 無視

ブロッキングをサポートするのは UserPromptSubmitPreToolUse のみです。SessionStartPostToolUseStop から 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(PreToolUsePostToolUse のみ有効)
timeout number デフォルト10秒。0 でタイムアウト無効

exit 2によるブロッキングの仕組み

Hookのシェルスクリプトが exit 2 を返すと、IBM Bobは現在のアクションをプログラムレベルで停止します。

  • UserPromptSubmitexit 2 → プロンプトがLLMに送信されない
  • PreToolUseexit 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

jqstdin の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") ![スクリーンショット 2026-08-21 16.24.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4385650/5e9d60dd-89ed-4163-84da-40be09622a76.png)

    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

実用的なユースケース例:

  1. JavaScript/TypeScript開発: ESLintの no-consoleno-debugger ルールを強制
  2. Python開発: Pylintで E0602(未定義変数)、F0401(import失敗)を検出
  3. Java開発: Checkstyleで命名規則、インデント、Javadoc必須を強制
  4. COBOL保守: 固定長フォーマット(80桁制限、カラム7インジケータ)を厳密に検査
  5. 企業コード: .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_diffinsert_contentsearch_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 を作成してください」と指示すると、以下のようにブロックされます:

スクリーンショット 2026-08-21 16.34.51.png

Step 2: COBOL固定長違反のテストケース
スクリーンショット 2026-08-21 16.24.54.png

      * 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原則

  1. 既存のLinterツールをそのまま活用する — ESLint、Pylint、Checkstyle等の標準ツールを command -v で検出し、プロジェクトの設定ファイル(.eslintrc.jsonpylintrccheckstyle.xml)をそのまま使う
  2. 言語別チェックと全言語共通チェックを分離する — 拡張子で言語を判定してLinterを切り替え、デバッグ出力・機密情報等の共通チェックは全言語に適用
  3. stdin は mktemp で一時ファイルに保存するINPUT=$(cat) でシェル変数に入れると改行を含むJSONでjqがparse errorを返す。mktemp 経由で jq -r '.field' "$TMPFILE" と読めば安全
  4. PostToolUseとStopでブロックしようとしない — この2つは exit 2 が効かない。事後処理(ログ・レポート)専用として使う

実用的な適用例

  • JavaScript/TypeScript: ESLintで no-consoleno-debuggerno-unused-vars を強制
  • Python: Pylintで E0602(未定義変数)、W0611(未使用import)を検出
  • Java: Checkstyleで命名規則、インデント、Javadoc必須を強制
  • COBOL: 固定長フォーマット(80桁制限、カラム7インジケータ)を厳密に検査
  • 企業コード: .license-header ファイルで定義したライセンスヘッダーの存在を強制
3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?