個人運営しているチャットアプリで、初めて Anthropic から「特定のリクエストパターンが疑わしいので確認したい」という連絡を受けたとき、私はとっさに「どのユーザーですか?」と返事ができませんでした。当時、リクエストには metadata.user_id を一切渡しておらず、サーバー側のログとも紐付けられない状態だったからです。
metadata.user_id は Messages API のリクエストにオプションで渡せる小さなフィールドで、ドキュメントには「ユーザーごとの ID を渡せる」とだけ書かれています。けれど、このフィールドが実際に何のために使われ、何を渡すと何が変わるのかを正面から説明している資料はあまり多くありません。私自身がアプリ運用で踏んだ落とし穴と、現在採用している実装パターンを共有します。
metadata.user_id は「abuse 検知のためのヒント」として設計されている
Anthropic の利用規約と API リファレンスを読み合わせると、metadata.user_id の役割は明確です。Anthropic 側が異常なリクエストパターンを検知したとき、それが「あなたのサービス全体の問題」なのか「特定の一人のユーザーの問題」なのかを区別するためのシグナルとして機能します。
このフィールドがないと、Anthropic から見えるのはあなたの API キーだけです。一人のユーザーが大量の不適切なプロンプトを投げた場合、その挙動はサービス全体の挙動として記録され、最悪の場合あなたの API キー全体に対して制限がかかる可能性があります。user_id を渡しておくと、Anthropic は「この user_id のリクエストだけが疑わしい」と判定でき、対応の粒度が細かくなります。
つまり、これは「あなたのアカウント全体を守るためのフィールド」と捉えるのが正しい理解です。ユーザートラッキングや分析のためのものではありません。
何を渡すと何が変わるか — 私の運用での実測
私の小規模なチャットアプリ(月間数千リクエスト)で、user_id あり/なしの状態を1ヶ月ずつ運用して比較したところ、Anthropic から発生した通知の文面に明確な違いが出ました。
user_id なしの期間に問題が起きたとき、メール本文には「貴サービス全体で〜」という抽象的な記述しかなく、私の側では全リクエストログを順に読み返す必要がありました。一方、user_id ありの期間では、通知メールに user_id: <ハッシュ値> が記載されており、自分のサービス側 DB との照合が一発で済みました。
さらに副次的な効果として、Anthropic 側の per-user レートリミット判定が user_id 単位で行われるようになるため、一人のユーザーがバーストしたときに他のユーザーが巻き添えで 429 を受け取る確率が下がります。これは小さなアプリでは見えにくい違いですが、ユーザー数が数百人を超えてくると体感できるようになりました。
生のメールアドレスや UUID をそのまま渡してはいけない
ここが最大の落とし穴です。私自身、最初の実装では metadata.user_id にユーザーのメールアドレスをそのまま入れていました。これは2つの意味で問題があります。
ひとつは、Anthropic のサーバーに自社ユーザーの個人情報が転送されてしまうこと。プライバシーポリシーや利用規約上、本人同意の範囲を超えてしまう可能性があります。もうひとつは、Anthropic 側のログに残った user_id が「メールアドレスそのもの」として読める状態になっていること。万が一の漏洩リスクに対しても明らかに脆弱です。
公式の推奨は「PII を含まない、自社内で安定した識別子」を渡すことです。私の現在の実装は、内部 user_id をサーバー側のシークレット(環境変数で管理)と組み合わせて HMAC-SHA256 でハッシュ化し、その先頭 16 文字を渡す形に統一しています。
# anthropic_user_id.py
import hashlib
import hmac
import os
# 環境変数で管理する固定ソルト(ローテーションは慎重に)
USER_ID_SALT = os.environ["ANTHROPIC_USER_ID_SALT"]
def to_anthropic_user_id(internal_user_id: str) -> str:
"""
内部ユーザー ID を HMAC で安定した擬似 ID に変換する。
同じ internal_user_id からは常に同じ値が返るため、
Anthropic 側でユーザー単位の追跡が可能になる。
"""
digest = hmac.new(
USER_ID_SALT.encode("utf-8"),
internal_user_id.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return digest[:16] # 先頭 16 文字で十分な一意性
# 期待出力: 同じ入力に対して常に同じ 16 文字が返る
# >>> to_anthropic_user_id("user_42")
# '8a3f1c9b2e4d6f01'ポイントは「ハッシュ化後の値が同じユーザーなら常に同じである」こと。Anthropic 側で「同一ユーザーからの連続リクエスト」と判定してもらうために、リクエストごとに変わるランダム値ではなく、内部 ID から決定論的に導出する必要があります。
ソルトを後から変更すると過去の user_id との連続性が切れてしまうため、運用開始前に決めて、原則として変更しないことを前提に置いてください。漏洩時の対応として変更するのは構いませんが、その場合は Anthropic 側の履歴とのリンクは切れる前提で運用判断します。
metadata を渡さない方がよい場面もある
すべてのケースで user_id を送るべきかというと、そうでもありません。私の中では次の場面では渡さない判断をしています。
- CLI ツールやローカル開発支援ツール: 「1 プロセス = 1 ユーザー」になっているので、API キー単位の判定で十分
- 社内向けの管理ツール: 利用者が限定されており、Anthropic 側で per-user に分離する意味が薄い
- 匿名性が重要なユースケース: 例えば相談系のサービスなど、サーバーログにすら user_id を残さない方針のもの
逆に、不特定多数のエンドユーザーを抱える Web サービスでは、ほぼ常に渡しておく方が安全側に倒れます。サービスの性質に応じて、user_id を「abuse 防御の保険として渡す」か「プライバシー方針に沿って意図的に渡さない」かを最初に決めておくと、後から方針がぶれません。
Messages API での実装
実際にリクエストに乗せる箇所はシンプルです。SDK の場合、metadata パラメータに dict を渡すだけで完結します。
import anthropic
from anthropic_user_id import to_anthropic_user_id
client = anthropic.Anthropic()
def chat(internal_user_id: str, prompt: str) -> str:
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
metadata={
# ハッシュ化済みの安定 ID を渡す
"user_id": to_anthropic_user_id(internal_user_id),
},
messages=[
{"role": "user", "content": prompt},
],
)
return response.content[0].text
# 使用例
# >>> chat("user_42", "今日の天気を教えてください")
# 'お住まいの地域を教えていただければ...'エラーハンドリングや stop_reason の扱いは Claude API の stop_reason を読み解く で詳しく書いていますので、合わせて参照してください。コスト最適化を併用したい場合は Claude API のプロンプトキャッシュがヒットしない時の診断手順 も役に立つはずです。
Web API 全般の安全設計を整理する一冊として手元に置いています。
ソルトのローテーションは慎重に
ハッシュ化を最初に導入したとき、私は ANTHROPIC_USER_ID_SALT を他のトークンと同じローテーション周期に乗せてしまい、四半期ごとに全ユーザーのハッシュ値が一斉に変わる状態を作ってしまいました。Anthropic 側から見ると、私のサービスのユーザーが定期的に「全員入れ替わる」ように見えてしまい、せっかく安定 ID を渡している意味がほぼ失われていました。
今は ANTHROPIC_USER_ID_SALT を「通常のシークレットローテーションには含めない長寿命の値」として明示的に Runbook に書いています。万が一の漏洩で本当にローテーションが必要になった場合は、Anthropic 側に蓄積されていた user_id ベースの履歴は途切れる前提で、一度きりのイベントとして調整します。短期的な連続性を犠牲にしてでも漏洩経路を断つ判断ですが、事前に決めておくと当日の運用判断が楽になります。
もう一点、実務上気をつけているのは「ハッシュ化後の値を自社ログにも残す」ことです。ハッシュ計算を都度メモリ上で行うだけにしてしまうと、数ヶ月後に「このハッシュは誰のものか」を追えなくなる場面があります。リクエストごとに「内部 ID とハッシュ値のペア」を1行ログに残しておくと、後から問い合わせが来てもすぐに紐付けられます。
次に試してほしいこと
まずは自分のサービスのユーザー識別子を1つ選び、上のサンプルにある to_anthropic_user_id を組み込んで、Messages API のリクエストに metadata.user_id を載せてみてください。サーバー側のログには「内部 ID とハッシュ化後の値の対応表」を残しておくと、Anthropic からの問い合わせがあったときにすぐに突合できます。
この一手間だけで、サービス全体の防御力が一段上がります。お読みいただきありがとうございました。