最近はナレッジをScrapboxに書き溜めています
GitHubでRequired Approvalsを設定したままRenovateのPRだけapproveなしでautomergeする
タイトルがややこしい。
Renovateによる依存アップデートのPRで、パッチとマイナーアップデートの場合はテストをPASSしていたら自動でマージして欲しい。
こういう設定をしている
{ "packageRules": [ { "matchUpdateTypes": ["minor", "patch"], "matchCurrentVersion": "!/^0/", "automerge": true } ] }
しかし、リポジトリのRulesetsでRequired Approvalsを設定しているとPRのApproveがないので永遠にマージされない。
でもRenovate以外の人間とAIによるPRはApprove必須にしたいため、この設定を剥がすことはできない。しかし大量の依存アップデートに対して人間がチマチマApproveすることは避けたい。
この二律背反問題を解消する必要がある。
解決方法
この問題と解消方法については以下のDocumentに記載がある。
- https://github.com/renovatebot/renovate-approve-bot を使う
- rulesetsのBYPASSを使う
1がラクなのだけど、落とし穴がありCODEOWNERファイルによる設定があると、この作戦はうまく機能しない。 これはBotをOWNERに指定できないからで、自動でReviewerにBotを追加できないから。
なのでCODEOWNER設定がある場合、2のパターンでBYPASSする必要がある。しかしこの設定がややこしい(というよりリファレンスがない)。
Rulesetsを分けてバイパス設定をする
設定のポイントは
- block force pushやstatus checkのrulesetから Requied ApprovalsとRestrict update のruleを分離しておく
- Bypasslist に OWNERとRenovateを追加し
Exempt from rulesにする

こうしておくと、Renovateが出すPRにはRequired Approvals設定が除外され、別のRulesetに設定しているStatus CheckによるCIが通ればautomergeによって自動でマージされていく。あとオーナーは自身のPRによる強制マージもできる。
対象ブランチのアップデートを保護により絞る場合、 Restrict updates のルールもRequired Approvedの設定側に分離しておく必要があり、これはmerge=target branch updateとなるからで、ややこしい。
Claude Codeの会話応答時間をStatuslineに表示する実装
この記事はClaude Codeとの共著で作成されました。実装の詳細や技術的な内容について、AIによる支援を受けて執筆しています。

Claude Codeを使っていて、1回の会話(プロンプト送信から応答完了まで)にどのくらい時間がかかっているかが気になった。特に複雑なタスクを依頼したとき、どの程度時間が経過しているのか分からなくなる。
Claude CodeからHookやStatuslineに渡されるコンテキスト情報を確認したが、会話の経過時間に関する情報は含まれていなかった。cost情報にはtotal_duration_msという項目があるものの、これは別の用途らしく、リアルタイムの会話時間は取得できない。
そのため、Hook機能を使って自分で時間を計測・記録するハック的な実装をすることにした。
全体の仕組み
Claude CodeにはHook機能があって、特定のイベントが発生したときにシェルコマンドを実行できる。これを使って以下の流れを作った:
- ユーザーがプロンプトを送信したとき→会話開始時刻を記録
- ツール実行中→会話の経過時間を更新
- 応答完了時→会話終了時刻を記録してステータスを完了に
- Statusline表示時→記録した時間データを読み取って表示
1回の会話ごとに時間を計測するので、新しいプロンプトが送信されるたびに計測がリセットされる。
状態の共有には一時ファイル(/tmp/claude-code-duration-{session_id}.json)を使った。
sequenceDiagram
participant User as ユーザー
participant CC as Claude Code
participant USH as user-prompt-submit-hook.sh
participant AIH as agent-in-progress-hook.sh
participant FRH as finished-responding-hook.sh
participant TMP as 一時ファイル
participant SL as Statusline
participant CCD as claude-code-duration.ts
User->>CC: プロンプト送信
CC->>USH: UserPromptSubmit Hook実行
USH->>TMP: 開始時刻を記録
loop ツール実行中
CC->>AIH: PreToolUse/PostToolUse Hook実行
AIH->>TMP: 経過時間を更新
end
CC->>FRH: Stop Hook実行
FRH->>TMP: 完了時刻を記録・status更新
loop Statusline表示
SL->>CCD: TypeScriptスクリプト実行
CCD->>TMP: 時間データを読み取り
CCD->>SL: フォーマット済み時間を返却
SL->>User: Statuslineに表示
end
settings.jsonの設定
.claude/settings.jsonにHookの設定を追加した:
{ "hooks": { "UserPromptSubmit": [ { "hooks": [ { "type": "command", "command": "~/bin/claude-utils/duration-logic-hooks/user-prompt-submit-hook.sh" } ] } ], "PreToolUse": [ { "hooks": [ { "type": "command", "command": "~/bin/claude-utils/duration-logic-hooks/agent-in-progress-hook.sh" } ] } ], "PostToolUse": [ { "hooks": [ { "type": "command", "command": "~/bin/claude-utils/duration-logic-hooks/agent-in-progress-hook.sh" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "~/bin/claude-utils/duration-logic-hooks/finished-responding-hook.sh" } ] } ] }, "statusLine": { "type": "command", "command": "bunx ccstatusline@latest", "padding": 0 } }
会話開始時の処理
user-prompt-submit-hook.shでは、プロンプト送信時に会話の開始時刻を記録する:
#!/bin/bash session_data=$(cat) session_id=$(echo "$session_data" | jq -r '.session_id // empty') if [ -z "$session_id" ]; then exit 0 fi tmp_file="${TMPDIR:-/tmp}/claude-code-duration-${session_id}.json" # 割り込み検知の処理 check_interrupt_in_transcript() { local transcript_path="$1" if [ -z "$transcript_path" ] || [ ! -f "$transcript_path" ]; then return 1 fi local last_line=$(grep -v '^$' "$transcript_path" | tail -n 1) local content=$(echo "$last_line" | jq -r '.message.content[0].text // .message.content // empty' 2>/dev/null) if [ "$content" = "[Request interrupted by user]" ]; then return 0 fi return 1 } current_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") if check_interrupt_in_transcript "$transcript_path" && [ -f "$tmp_file" ]; then # 割り込み後の継続 existing_session_id=$(cat "$tmp_file" | jq -r '.sessionId') existing_start=$(cat "$tmp_file" | jq -r '.startTimestamp') existing_duration=$(cat "$tmp_file" | jq -r '.duration // 0') cat > "$tmp_file" << EOF { "sessionId": "$existing_session_id", "startTimestamp": "$existing_start", "lastUpdate": "$current_timestamp", "duration": $existing_duration, "status": "interrupted" } EOF else # 新規会話 cat > "$tmp_file" << EOF { "sessionId": "$session_id", "startTimestamp": "$current_timestamp", "lastUpdate": "$current_timestamp", "duration": 0, "status": "active" } EOF fi
割り込み検知の部分が少し複雑だった。transcriptファイルを読んで最後の行に[Request interrupted by user]があるかチェックしている。
進行中の処理
agent-in-progress-hook.shでは、ツール実行前後に経過時間を更新する:
#!/bin/bash session_data=$(cat) session_id=$(echo "$session_data" | jq -r '.session_id // empty') tmp_file="${TMPDIR:-/tmp}/claude-code-duration-${session_id}.json" if [ ! -f "$tmp_file" ]; then exit 0 fi existing_data=$(cat "$tmp_file") start_timestamp=$(echo "$existing_data" | jq -r '.startTimestamp') current_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # ISO8601からUnixエポック秒への変換 start_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$start_timestamp" +%s 2>/dev/null || date -d "$start_timestamp" +%s 2>/dev/null) current_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$current_timestamp" +%s 2>/dev/null || date -d "$current_timestamp" +%s 2>/dev/null) duration=$(( (current_epoch - start_epoch) * 1000 )) echo "$existing_data" | jq \ --arg current_timestamp "$current_timestamp" \ --argjson duration "$duration" \ '.lastUpdate = $current_timestamp | .duration = $duration | .status = "active"' \ > "$tmp_file"
時刻の扱いが面倒だった。ISO8601形式でUTCで記録して、計算時にUnixエポック秒に変換している。macOSとLinuxでdateコマンドの使い方が違うので両対応した。
完了時の処理
finished-responding-hook.shでは、応答完了時に最終時刻を記録してstatusを"finished"に変更する。中身はagent-in-progress-hook.shとほぼ同じで、最後にstatusを変える部分だけ違う:
echo "$existing_data" | jq \ --arg current_timestamp "$current_timestamp" \ --argjson duration "$duration" \ '.lastUpdate = $current_timestamp | .duration = $duration | .status = "finished"' \ > "$tmp_file"
Statusline表示の実装
最初はStatuslineでだけTypeScriptで実装していた。後からHook機能を追加することにしたとき、Claude Codeに相談したらシェルスクリプトで書き始めたのでそのまま使った。深い意味はない。
TypeScriptで時間データを読み取って表示する部分:
#!/usr/bin/env bun import { readFileSync, existsSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; interface StatuslineInput { session_id: string; cost: { total_duration_ms: number; }; } interface DurationData { sessionId: string; startTimestamp: string; lastUpdate: string; duration: number; status: "active" | "finished" | "interrupted"; } function parseStatuslineInput(input: string): StatuslineInput | null { try { return JSON.parse(input); } catch { return null; } } function readDurationData(sessionId: string): DurationData | null { try { const tmpFile = join(tmpdir(), `claude-code-duration-${sessionId}.json`); if (!existsSync(tmpFile)) { return null; } const content = readFileSync(tmpFile, "utf-8"); const data = JSON.parse(content) as DurationData; return data; } catch { return null; } } function formatDuration(durationMs: number): string { const totalSeconds = Math.floor(durationMs / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; const parts: string[] = []; if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); parts.push(`${seconds}s`); return parts.join(" "); } function formatDurationString(durationMs: number, status: string): string { const formattedTime = formatDuration(durationMs); const statusIcon = status === "finished" ? "✅" : status === "interrupted" ? "🗣️" : "💭"; return `${statusIcon} ${formattedTime}`; } async function main() { try { let input = ""; for await (const chunk of process.stdin) { input += chunk.toString(); } const statuslineData = parseStatuslineInput(input); if (!statuslineData) { console.log("❌ Parse error"); return; } const durationData = readDurationData(statuslineData.session_id); if (!durationData) { console.log(formatDurationString(0, "active")); return; } console.log(formatDurationString(durationData.duration, durationData.status)); } catch { console.log("❌ Error occurred"); } } if (import.meta.main) { await main(); }
ccstatuslineからセッション情報がJSON形式でstdinに渡されてくるので、それを解析してsession_idを取得し、対応する一時ファイルを読み取って時間を表示する。
ステータスに応じてアイコンを変えた: - 💭 進行中 - ✅ 完了 - 🗣️ 割り込み
実際の表示
Statuslineの設定ではbunx ccstatusline@latestを実行して、その中で上記のTypeScriptスクリプトが呼ばれる仕組みになっている。ccstatuslineは汎用のStatusline表示ツールで、カスタムコマンドも実行できる。
作ってみて分かったこと
Hook機能を使うことで、Claude Codeの動作に合わせて任意の処理を挟むことができた。一時ファイルを使った状態共有は単純だけど確実に動く。
時刻の扱いが一番面倒だった。UTCで統一してISO8601形式で保存し、計算時にUnixエポック秒に変換する方法に落ち着いた。
割り込み処理も考えていなかったが、transcriptファイルを見れば判定できることが分かった。これで中断後の再開時にも時間が引き継がれる。
Claude Codeは拡張性が高くて、こういう細かいカスタマイズができるのがいい。1回の会話時間が可視化されることで、どの程度の処理時間がかかっているかが把握しやすくなった。
とはいえ、これはハック的な実装なので、公式に会話時間の情報が提供されるようになったらいいなと思う。GitHubにも同様の要望が上がっているようで(Issue #5852)、将来的には標準機能として対応してもらえるかもしれない。