agentsclimarketplace

Hook log append pattern

Skill bokuwalily/claude-code-skills/skills/hook-log-append-pattern

Claude Code の Stop/PostToolUse フックから構造化データを JSONL ログに追記する汎用パターン。stop_cost_log.sh / post_audit_log.sh / stop_agent_tracker.sh のような「stdin → parse → append」ループを新規で書くときに参照する。From its SKILL.md

Install
npx -y skills add bokuwalily/claude-code-skills --skill hook-log-append-pattern

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

4.7 KB, ~1.4k tokens by cl100k_base, as published. Nobody here has run it

Procedure

1. hook stdin の構造

Stop フック・PostToolUse フック両方とも Claude Code から 標準入力 で JSON ペイロードを受け取る。

INPUT=$(cat)
[ -z "$INPUT" ] && exit 0   # 空入力は即 exit 0(エラーにしない)
フック種別主要フィールド
Stopsession_id, transcript_path, cwd, hook_event_name
PostToolUsetool_name, tool_input, tool_response, session_id

2. 追記先ファイルの準備

LOG_DIR="$HOME/.claude/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/my-feature.jsonl"
  • .jsonl 形式(1行1JSON)にすると jq でのフィルタリングが容易
  • 月別ファイル分割は "$LOG_DIR/$(date +%Y-%m).jsonl" で簡単に実現

3. bash + inline python3 パターン(推奨)

外部依存ゼロ。python3 は macOS/Linux 標準搭載。

export HOOK_INPUT="$INPUT"
export LOG_PATH="$LOG_FILE"

python3 - <<'PY'
import os, sys, json, datetime

try:
    data = json.loads(os.environ.get("HOOK_INPUT", ""))
except Exception:
    sys.exit(0)

# --- 必要フィールドを抽出 ---
sid = data.get("session_id", "")
cwd = data.get("cwd", "")

record = {
    "ts":         datetime.datetime.now().isoformat(timespec="seconds"),
    "session_id": sid,
    "cwd":        cwd,
    # 追加したいフィールドをここに
}

with open(os.environ["LOG_PATH"], "a", encoding="utf-8") as f:
    f.write(json.dumps(record, ensure_ascii=False) + "\n")
PY

exit 0   # フックは必ず 0 で終わる(非0 は Claude Code への拒否シグナル)

4. transcript_path を使った処理

Stop フックでは transcript_path が渡る。存在チェックを必ずする:

tp = data.get("transcript_path", "")
if not tp or not os.path.exists(tp):
    sys.exit(0)

with open(tp, "r", encoding="utf-8", errors="replace") as f:
    for line in f:
        try:
            rec = json.loads(line)
        except Exception:
            continue
        # rec を処理して record に集計

5. 重複防止(Stop フックで同セッションを二重記録しない)

seen_ids = set()
if os.path.exists(LOG_PATH):
    with open(LOG_PATH) as f:
        for line in f:
            try:
                r = json.loads(line)
                if r.get("session_id") == sid:
                    seen_ids.add(r.get("unique_key"))
            except Exception:
                continue
# 書き込み前に seen_ids チェック

6. フックの接続(settings.json)

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/.claude/hooks/my_log_hook.sh"
          }
        ]
      }
    ]
  }
}

PostToolUse はツール名で絞り込める:

"PostToolUse": [
  {
    "matcher": "Write|Edit",
    "hooks": [{"type": "command", "command": "$HOME/.claude/hooks/audit_log.sh"}]
  }
]

Pitfalls

  • exit 0 を忘れると Claude Code がブロックされる: フックが非 0 で終了すると操作が拒否される。try/except で必ず sys.exit(0) を呼ぶ
  • フルスキャン問題: Stop フックは毎回 transcript を全行読む。セッションが長い(1000行+)と遅くなる。重い処理は & でバックグラウンド化し exit 0 だけ先に返す
  • stat -f %m は macOS 専用: タイムスタンプ取得は Linux で stat -c %Y。クロスプラットフォームなら python3 -c "import os; print(os.path.getmtime(...))" が安全
  • env 経由の文字列長: export HOOK_INPUT="$INPUT" で渡せるサイズは OS 制限(通常 256KB)。大きなペイロードは tmpfile 経由にする
  • LOG_DIR が存在しない: mkdir -p を常に先頭で実行する。cron/launchd 起動時はホームが確定しないことがある

Verification

# 直近エントリを確認
tail -3 ~/.claude/logs/my-feature.jsonl | jq .

# ログの整合性チェック
jq -c . ~/.claude/logs/my-feature.jsonl | wc -l  # 壊れた行があれば jq がエラーを出す

# hook を手動トリガー(Stop hook は stdin が必要)
echo '{"session_id":"test","transcript_path":"","cwd":"/tmp","hook_event_name":"Stop"}' \
  | bash ~/.claude/hooks/my_log_hook.sh
echo "exit=$?"

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.