Claude Media
Claude CodeのラッパーでSlackスレッドを識別する — Claude Tagの環境変数

Claude CodeのラッパーでSlackスレッドを識別する — Claude Tagの環境変数

セルフホスト環境のラッパーには、Claude TagのSlackスレッドを示す変数が3つ届きます。いつ未設定になるか、スレッドのtsをチャンネルIDと組にして使う理由を解説します。

セルフホスト環境のラッパースクリプトは、Claude TagのSlackスレッドをCLAUDE_CODE_REMOTE_SLACK_THREAD_TS・CLAUDE_CODE_REMOTE_SLACK_THREAD_URL・CLAUDE_CODE_REMOTE_SLACK_CHANNEL_IDの3変数で識別できます。ただし3つとも未設定になりうるため、変数ごとに既定値を付けて扱う必要があります。

Claude Tagのチャンネルセッションは、組織共有の環境かセルフホスト環境のどちらかで動きます。後者を選んだ組織では、Slackのどのスレッドから起動されたセッションなのかを、Runnerホスト側のログや課金の按分に残したくなります。その入口がラッパーです。ラッパーの全体像はラッパースクリプトとフックの解説にあります。ここでは、Slack関連の変数に絞って、渡る条件と落とし穴、書き方を扱います。

ラッパーに届くSlack関連の変数は何か

Runnerは、セッションごとにラッパーを1回起動します。このとき渡る環境変数のうち、スレッドの識別に使えるのは次の4つです。

変数中身
CLAUDE_CODE_REMOTE_SESSION_UUID中身セッションIDを標準的なUUID形式にしたもの。UUIDで管理する外部システムとの突き合わせ用
CLAUDE_CODE_REMOTE_SLACK_THREAD_URL中身1つのSlackスレッドに属するClaude Tagセッションで、そのスレッドへのリンク
CLAUDE_CODE_REMOTE_SLACK_THREAD_TS中身同じく、そのスレッドのSlackタイムスタンプ(1700000000.000100の形式)
CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID中身セッションが担当する、またはスレッドが属するSlackチャンネルかDMのID(C0123ABCDEの形式)

セッションIDにはcse_...形式のCLAUDE_CODE_REMOTE_SESSION_IDもあります。ライフサイクルフックがCLAUDE_RUNNER_SESSION_IDとして見るのは、session_...形式の同じセッションです。UUID形式の値はこれらのあいだで一致します。外部のチケットや監査ログとセッションを結ぶなら、UUID形式が扱いやすい選択肢です。

3つのSlack変数は、いずれもClaude Tagのセッション向けです。Claude Tag以外のセッションでは、URLとチャンネルIDは未設定です。

どの変数がどの条件で渡るのか

渡る条件は変数ごとに少しずつ違います。URLとTSは「1つのスレッドに属するセッション」の変数で、チャンネルIDは「スレッドの有無を問わずClaude Tagのセッション」の変数です。

セッションの種類THREAD_URLTHREAD_TSCHANNEL_ID
Claude Tag以外THREAD_URL未設定THREAD_TS記載なしCHANNEL_ID未設定
Claude Tagでスレッドに属するTHREAD_URL未設定の場合もあるTHREAD_TS未設定の場合もあるCHANNEL_ID未設定の場合もある
Claude Tagでスレッドに属さないTHREAD_URL未設定THREAD_TS記載なしCHANNEL_ID未設定の場合もある

表の「記載なし」は、公式の説明がスレッドに属するセッションの条件しか書いていない欄です。スレッド付きの行は3つとも「未設定の場合もある」になっている点に注意してください。スレッドに属するセッションでも、3つがそろって届くとは限りません。とくにTSは、URLが未設定のときに設定されることがあります。そのため「URLがあればTSもある」という前提でスクリプトを書くことはできません。変数ごとに、独立して存在を確かめます。

届く場所にも制限があります。これらの変数を読めるのは、ラッパー、commandフック、そしてセッションが実行するもの(シェルコマンド、gitフック、Claude Codeのフック)です。checkout・post-session・spawn-runnerの各フックには渡りません。たとえばpost-sessionフックでスレッド単位の後片付けをしたいときは、ラッパーの段階でスレッドの情報をファイルなどに書き出しておく設計になります。spawn-runnerの動きはオンデマンドRunnerの記事にまとめています。

未設定になりうる変数に既定値を付ける理由

ラッパーの先頭にset -uを書くスクリプトは、多いはずです。未定義の変数を使った時点でエラー終了させ、打ち間違いを早く見つけるためです。ところがset -uの下で未設定のSlack変数をそのまま展開すると、Bashがunbound variableで止まります。

止まったラッパーは、execまで到達しません。Claude Tag以外のセッションではURLとチャンネルIDが入らないため、同じラッパーを共用している環境では、Claude Tagを経由しないセッションだけが毎回止まる、という形で現れます。

回避は、展開のたびに既定値を付けることです。"${CLAUDE_CODE_REMOTE_SLACK_THREAD_TS:-}"のように、:-の後を空にするだけで足ります。ドキュメントは、このルールを次の5変数に適用するよう示しています。

  • CCR_SESSION_ACCOUNT_EMAIL
  • CLAUDE_RUNNER_CLIENT_PLATFORM
  • CLAUDE_CODE_REMOTE_SLACK_THREAD_URL
  • CLAUDE_CODE_REMOTE_SLACK_THREAD_TS
  • CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID

前の2つは、Slackと関係のない変数です。メールはトークンに作成者のメールが無いとき、クライアント種別は記録が無いときに未設定になります。ラッパー全体をset -uで守るなら、この5つを漏れなく既定値付きにするのが安全です。

もう1つの注意は引用符です。変数は必ず"..."で囲みます。スレッドのURLには?や&が含まれうるため、囲まないとシェルが別の意味に解釈します。evalやsh -cに渡す文字列へ値を直接埋め込むのも避け、文字列の側から変数を参照させます。

スレッドのtsはチャンネルIDと組にして記録する

スレッドのタイムスタンプは、チャンネルの中でメッセージを区別する値です。別のチャンネルには、同じ値のメッセージが存在しうるので、tsだけを記録キーにすると、複数のチャンネルでClaude Tagを使っている組織では衝突の可能性があります。ドキュメントも、CLAUDE_CODE_REMOTE_SLACK_CHANNEL_IDをCLAUDE_CODE_REMOTE_SLACK_THREAD_TSと一緒に記録するよう求めています。

組み立て方は単純です。チャンネルIDとtsが両方そろったときだけC0123ABCDE:1700000000.000100のような複合キーを作り、片方でも欠けたら「キー無し」として扱います。欠けたまま片方だけで代用すると、衝突や誤った紐づけの原因になるからです。

次のラッパーは、この考え方をそのまま書いた一例です。保存先のログのパスと記録する項目は、運用に合わせて置き換えてください。

#!/bin/bash
set -u
 
channel="${CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID:-}"
thread_ts="${CLAUDE_CODE_REMOTE_SLACK_THREAD_TS:-}"
thread_url="${CLAUDE_CODE_REMOTE_SLACK_THREAD_URL:-}"
session_uuid="${CLAUDE_CODE_REMOTE_SESSION_UUID:-unknown}"
 
if [ -n "$channel" ] && [ -n "$thread_ts" ]; then
  thread_key="${channel}:${thread_ts}"
else
  thread_key="none"
fi
 
printf 'session=%s thread_key=%s url=%s\n' \
  "$session_uuid" "$thread_key" "${thread_url:-none}" \
  >> "${SESSION_AUDIT_LOG:-/var/log/claude-session-audit.log}"
 
exec "$CLAUDE_RUNNER_CLAUDE_BIN" "$@"

作りは3段です。冒頭で4つの変数を既定値付きで取り出し、チャンネルIDとtsが両方あるときだけ複合キーを作り、最後にログを残してexecします。execでRunner自身のバイナリへ処理を渡す形は、シグナルや終了コードを正しく伝えるための、ラッパーの共通の締め方です。引数の"$@"を落とすと、システムプロンプトのフラグも失われます。

ここでは、ログ先をSESSION_AUDIT_LOGで差し替えられるようにしました。実際のRunnerなしでも、手元で動作を確かめられます。

Runnerなしでラッパーの分岐を確かめる

CLAUDE_RUNNER_CLAUDE_BINに/bin/echoを指して、必要な変数だけを手で与えれば、Slackにつながなくても分岐を試せます。次は上のスクリプトを手元のbashで実行した結果です(UUIDとIDはダミーです)。

export SESSION_AUDIT_LOG=./audit.log CLAUDE_RUNNER_CLAUDE_BIN=/bin/echo
 
# スレッドに属するClaude Tagのセッション
CLAUDE_CODE_REMOTE_SESSION_UUID=11111111-2222-3333-4444-555555555555 \
CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID=C0123ABCDE \
CLAUDE_CODE_REMOTE_SLACK_THREAD_TS=1700000000.000100 \
  ./wrapper.sh --version
 
# Slack変数が何も無いセッション
./wrapper.sh --version
 
cat audit.log

2回の実行はどちらも--versionをechoして終わります。ラッパーが引数を落とさずにexecしていることの確認になります。audit.logには次の2行が残りました。

session=11111111-2222-3333-4444-555555555555 thread_key=C0123ABCDE:1700000000.000100 url=none
session=unknown thread_key=none url=none

同じ確認で、チャンネルIDだけ・tsだけを与えた場合は、どちらもthread_key=noneになりました。片方が欠けたセッションに、中途半端なキーが残らないことが確かめられます。

逆に、:-を外して"$CLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID"と書いた版では、Slack変数なしの実行がCLAUDE_CODE_REMOTE_SLACK_CHANNEL_ID: unbound variableで止まりました。既定値を省くとどうなるかを、そのまま再現できます。Claude Tag以外のセッションで同じ症状が出たら、まずこの形の書き方を疑います。

セッションの中からも読める — コミットにスレッドのリンクを残す

変数が届く先はラッパーだけではありません。セッションが実行するシェルコマンド、gitフック、Claude Codeのフックからも読めます。execで子プロセスに処理を渡せば、ラッパーの環境がそのまま引き継がれるためです。

使い道の一例が、コミットメッセージへのスレッドのリンクの追記です。リポジトリのprepare-commit-msgフックに、次のようなスクリプトを置きます。これは例で、公式のサンプルではありません。

#!/bin/sh
# $1 はコミットメッセージのファイル
url="${CLAUDE_CODE_REMOTE_SLACK_THREAD_URL:-}"
[ -n "$url" ] || exit 0
printf '\nSlack-Thread: %s\n' "$url" >> "$1"

URLが未設定のセッションでは、何も足さずに終わります。ここでも既定値を付けた展開と、空のときの早期終了が要になります。URLを持たないがtsとチャンネルIDはある、というセッションに備えるなら、複合キーをSlack-Thread-Key:として足す分岐を加えます。

後からコミットを見た人が、元の依頼文脈をSlackで追えるようになります。一方、リンクを開けるのはそのワークスペースのメンバーだけです。公開リポジトリに残すと、リンク先を持たない第三者には意味のない文字列になり、チャンネルの存在を外部に知らせる結果にもなります。社内リポジトリに限って使う、といった線引きが必要です。

変数が空のときの切り分け

ログにthread_key=noneばかり残る、というときは、次の順で原因を絞ります。

切り分け

thread_keyがnoneになる原因を順に確かめる

  1. QそのセッションはClaude Tag経由ですか?
    答えがいいえのとき

    Claude Tag以外のセッションでは、URLとチャンネルIDは設定されません。noneは正常な結果です。

    答えがはい
  2. Qラッパーは`--exec-path`か`command`フックで動いていますか?
    答えがいいえのとき

    変数が届くのはラッパーとcommandフックです。checkoutやpost-sessionのフックで読んでいるなら、ラッパーで書き出した値を渡す形に変えます。

    答えがはい
  3. QチャンネルIDとtsの両方が空ではありませんか?
    答えがいいえのとき

    どちらかが未設定なら、上のスクリプトは意図どおりnoneにします。スレッドに属さないセッションや、公式が「未設定になりうる」と書く場合に当たります。

    答えがはい

それでも埋まらないとき

セッションの種類(スレッドか、チャンネル全体か)を、Claude Tagの管理画面とSlack側の呼び出し方で確かめます。

セッションの出自を判別するほかの手がかり

ラッパーの変数が使えないときや、集計の側で見分けたいときの手がかりもあります。OpenTelemetryのメトリクスでは、OTEL_METRICS_INCLUDE_ENTRYPOINT=trueを設定するとapp.entrypointが付き、Claude Tagのセッションはclaude-in-slackになります。これは環境の変数として設定する方法で、Claude Tagのセッションにも届く経路です。

誰がタグ付けしたかを、メトリクスのuser.*属性から読むのは避けてください。Claude Tagのチャンネルセッションでは、Claudeは特定のメンバーではなく組織の共有アイデンティティとして動くからです。「どのスレッドか」はラッパーの3変数で、「Claude Tagのセッションか」はapp.entrypointで、という分担になります。

運用で気をつけること

ログに残す値は、最小限にとどめます。

  • スレッドのURL: ?や&を含みうる文字列です。保存先のアクセス権は、Slackの権限に合わせて設計します
  • トークン類: CLAUDE_CODE_OAUTH_TOKENなどは書き出さず、ログにも出しません。寿命は約30分でも、漏れれば持ち主として振る舞えます
  • 標準エラー出力: ラッパーか子プロセスが非ゼロで終了すると、標準エラーの末尾がセッションに投稿され、Slackの利用者に見えます。set -xは本番に持ち込みません

Runnerの台数にも関係します。Runnerは1人のオーナーのセッションだけを受け持つ作りで、Claude Tagのエージェントも1人分として数えられます。同時に動くユーザーとエージェントの数が、最低限のRunner台数の目安になります。台数の考え方は本番運用の記事を参照してください。

まとめ

Slackのスレッドを識別したいラッパーは、4つの変数を取り出して、チャンネルIDとtsの両方がそろったときだけ複合キーにします。取り出すときは必ず:-で既定値を付け、引用符で囲みます。set -uで止まるラッパーは、Slack変数の入らないセッションを失敗させるので、本番に入れる前にSlack変数なしの実行で一度試しておくのが確実です。Claude TagとClaude Code本体の関係は、Slack連携の違いを比べた記事で扱っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/self-hosted-environments-configuration
  2. code.claude.com/docs/en/cloud-environments
  3. code.claude.com/docs/en/monitoring-usage
この記事を共有:XはてブLinkedIn