
Claude Codeで通常ログインとsetup-token認証のOTelテレメトリを比較してみた
Claude Codeにはclaude setup-tokenで1年間有効なOAuthトークンを発行し、CLAUDE_CODE_OAUTH_TOKENに渡して認証する経路があります。
トークン発行自体を管理側で止める機能はなく(2026/8時点)、流出時の被害もuser:inferenceスコープの範囲に限られます。
今回気になったのは別の点です。テレメトリを見て「誰が通常ログインで、誰がこのトークンを使っているか」を判別できるか、です。
claude auth statusを実行すると、トークンが実際に使われている限り、/loginの永続クレデンシャルの有無にかかわらずauthMethod: "oauth_token"(プロフィール情報なし)を返します。
ではOTelのテレメトリも同じように「トークンだけの匿名なセッション」に見えるのか。ローカルにOTel Collectorを立て、通常ログインのセッションとトークン認証のセッションで同じプロンプトを流し、出力される属性を突き合わせて確かめました。
結論から言うと、OTelのログだけでトークン認証かどうかを判別できるかは、その端末に/loginの実績があるかどうかで変わります。
/loginをしたことがある端末では、実際の認証方式がトークンでも、OTelのイベントにuser.emailやorganization.idが通常ログインと同じ値でそのまま乗ります。claude auth statusはプロフィール情報を隠すのに、OTel側は隠さないため、ログだけでは通常ログインとの区別が付きません。
逆に/loginを一度も実行していない端末では、これらの属性が丸ごと欠落します。/loginを経ずに認証できている以上、この場合はトークン利用だと判断できます。
確認したいこと
setup-tokenで発行したトークンのスコープはuser:inferenceのみで、発行自体を止める管理機能もありません(前述の記事の通り)。止められないなら、使われたことが後から分かるかどうかが気になるところです。
確認したいことは3点です。
- トークン認証のセッションで、そもそも認証イベントが出るのか
- 認証イベント以外の、全イベントに付与される属性(
user.email、organization.id、user.id等)に差分が出るのか claude auth statusが返す情報と、OTelが記録する情報は一致するのか
2点目はsetup-tokenのトークンのスコープに理由があります。user:profileを含まないため、claude auth statusでもプロフィール情報が表示されません。同じ理屈なら、イベント属性のuser.emailやorganization.idも欠落するはずです。実際には、環境によって結果が分かれました。
なお、Claude Enterpriseには設定変更を対象にしたCompliance API経由の監査ログ機能もありますが、今回の検証範囲外です(Configure server-managed settings - Claude Code Docs)。
検証環境
確認方法はClaude Codeのテレメトリで会話の生ボディまで見る記事と同じ構成を用います。ローカルにOTel Collectorをdockerで立て、受け取ったログ・メトリクスをdebugエクスポーターで標準出力に流します。
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
debug:
verbosity: detailed
service:
pipelines:
logs:
receivers: [otlp]
exporters: [debug]
metrics:
receivers: [otlp]
exporters: [debug]
docker run --rm -d --name otel-collector -p 4317:4317 -p 4318:4318 \
-v "$(pwd)/otel-collector-config.yaml":/etc/otelcol/config.yaml:ro \
otel/opentelemetry-collector:latest
Claude Code側は前掲記事と同じ環境変数を使います。会話本文の中身は今回の論点と無関係なので、OTEL_LOG_RAW_API_BODIESは付けません。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_LOG_USER_PROMPTS=1
比較した3つの実行
認証方式以外の条件をそろえ、同じプロンプトでclaude -p(非対話)を1ターンずつ実行しました。
claude -p "Reply with exactly one word: pong"
pong
- Run A(通常ログイン): 手元の作業端末で
/loginで認証済みの状態のまま実行 - Run B(トークン認証、
/loginあり): 同じ作業端末でclaude setup-tokenを新規発行し、CLAUDE_CODE_OAUTH_TOKENに渡して実行。/loginの永続クレデンシャルは残したまま - Run C(トークン認証、
/login未実施):/loginを一度も実行していない素のDockerコンテナで、Run Bで発行したトークンを設定して実行(詳細は後述)
claude setup-tokenは1年間有効なOAuthトークンを発行する操作です。発行したトークンは検証後にclaude.aiのSettings > Claude Codeから失効させています。トークンの値はターミナルに一度しか表示されないため、控えたらすぐ環境変数に設定し、メモや別ファイルには残さないようにしました。
export CLAUDE_CODE_OAUTH_TOKEN=<発行されたトークンの値>
Run Bがトークンで認証されているかは、claude -p実行後にclaude auth statusを実行して確認します。
{
"loggedIn": true,
"authMethod": "oauth_token",
"apiProvider": "firstParty"
}
/loginの永続クレデンシャルは手元の作業端末に残ったままですが、authMethodは"oauth_token"になり、emailやorgIdなどのプロフィール情報も一切出ません。公式ドキュメントの認証優先順位(Authentication - Claude Code Docs)でCLAUDE_CODE_OAUTH_TOKENは/loginより上位に明記されており、実測もその通りでした。
- Cloud provider credentials(Bedrock/Vertex/Foundry)
ANTHROPIC_AUTH_TOKENANTHROPIC_API_KEYapiKeyHelperCLAUDE_CODE_OAUTH_TOKEN/loginのSubscription OAuth credentials
認証イベントは出るか
出ませんでした。Run A・Run B・Run Cのいずれも、claude_code.authというイベント自体が一度も出力されていません。
Run Aで実際に採取したイベントの1件はこのような形です。(認証イベントではなく、セッション開始時のhookイベント)
LogRecord #0
Body: Str(claude_code.hook_execution_start)
Attributes:
-> user.id: Str(<USER_ID_HASH>)
-> session.id: Str(<SESSION_ID>)
-> organization.id: Str(<ORGANIZATION_ID>)
-> user.email: Str(<USER_EMAIL>)
-> user.account_uuid: Str(<ACCOUNT_UUID>)
-> user.account_id: Str(<ACCOUNT_ID>)
-> terminal.type: Str(non-interactive)
-> event.name: Str(hook_execution_start)
-> hook_event: Str(SessionStart)
-> hook_name: Str(SessionStart:startup)
公式ドキュメント(Monitoring - Claude Code Docs)で確認したところ、auth_methodが乗るclaude_code.authイベントは「/loginまたは/logoutが完了したときに記録される」専用のイベントでした。
今回のいずれの実行も、既にログイン済み(あるいはトークンをセットした状態)のままclaude -pを叩いただけで、/loginコマンド自体は実行していません。そのため、認証方式によらずこのイベントは発火しません。
「トークン認証だから出ない」のではなく、「セッション開始のたびに出るイベントではない」というのが実態でした。CI/CDやスクリプトからの定常的な利用シーンでは、認証方式を問わずこのイベントは観測できないことになります。
全イベントに乗る属性は同じか
Run A・Run Bの全イベント(claude_code.user_prompt / claude_code.api_request / claude_code.assistant_response等)を突き合わせたところ、属性のキー・値ともに差はありませんでした。
| 属性 | Run A(通常ログイン) | Run B(トークン認証) |
|---|---|---|
user.email |
<USER_EMAIL> |
<USER_EMAIL>(同じ値) |
organization.id |
<ORGANIZATION_ID> |
<ORGANIZATION_ID>(同じ値) |
user.account_uuid |
<ACCOUNT_UUID> |
<ACCOUNT_UUID>(同じ値) |
user.account_id |
<ACCOUNT_ID> |
<ACCOUNT_ID>(同じ値) |
user.id |
<USER_ID_HASH> |
<USER_ID_HASH>(同じ値) |
terminal.type |
non-interactive |
vscode(実行環境の違い) |
claude auth statusはRun Bでプロフィール情報を一切出さなかったのに、OTelのイベントにはuser.email以下がRun Aとまったく同じ値で乗っていました。setup-tokenのトークンはuser:profileスコープを持たないため、claude auth statusの挙動としては正しいはずですが、OTel側には同じ値がそのまま記録されています。
トークンで認証した環境でも/login のクレデンシャルが存在する場合は、OTelのテレメトリには通常ログインと同じレベルで個人が特定できる情報が乗っています。
/loginを一度もしていない環境ではどうか(Run C)
Run A・Run Bはどちらも、同じ作業端末に検証前から/loginの永続クレデンシャルが存在する状態でした。プロフィール情報がそのキャッシュ由来なのか、認証方式によらず常に付与されるものなのかを切り分けるため、/loginを一度も実行していない、まっさらな環境を用意して追試しました。
node:22-slimの素のDockerコンテナにnpm install -g @anthropic-ai/claude-codeでCLIをインストールした状態(Run C)です。コンテナ起動時に--add-hostでホスト側のCollectorへの経路を用意しておきます。
docker run --rm -d --name claude-isolated-test \
--add-host=host.docker.internal:host-gateway \
-v "$(pwd)":/work -w /work node:22-slim tail -f /dev/null
コンテナ内の環境変数は、Run A・Run Bと同じものに加え、OTEL_EXPORTER_OTLP_ENDPOINTだけホスト側Collectorのアドレスに変えます。
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_EXPORTER_OTLP_ENDPOINT=http://host.docker.internal:4317
export OTEL_LOG_USER_PROMPTS=1
コンテナ内でトークン設定前にclaude auth statusを実行すると{"loggedIn":false,"authMethod":"none","apiProvider":"firstParty"}となり、まっさらであることを確認しています。
トークンはRun Bで発行したものをそのまま使い回し、コンテナ内でCLAUDE_CODE_OAUTH_TOKENとして設定しています。新しく発行し直すと余計な変数が増えるので、変えたのは実行環境だけです。
export CLAUDE_CODE_OAUTH_TOKEN=<発行されたトークンの値>
トークン設定後は、コンテナ内でRun A・Run Bと同じclaude -p "Reply with exactly one word: pong"を実行しています。Run CでもRun Bと同じくclaude auth statusはoauth_token(プロフィール情報なし)でした。ここまではRun Bと同じですが、テレメトリの中身ははっきり違いました。
| 属性 | Run A(/login) |
Run B(/loginありの環境でトークン認証) |
Run C(/login未実施の分離環境でトークン認証) |
|---|---|---|---|
user.email |
<USER_EMAIL> |
<USER_EMAIL>(Run Aと同じ) |
属性自体が存在しない |
organization.id |
<ORGANIZATION_ID> |
<ORGANIZATION_ID>(Run Aと同じ) |
属性自体が存在しない |
user.account_uuid |
<ACCOUNT_UUID> |
<ACCOUNT_UUID>(Run Aと同じ) |
属性自体が存在しない |
user.account_id |
<ACCOUNT_ID> |
<ACCOUNT_ID>(Run Aと同じ) |
属性自体が存在しない |
user.id |
<USER_ID_HASH> |
<USER_ID_HASH>(Run Aと同じ値) |
<USER_ID_HASH_C>(Run A/Bとは別の値) |
Run Cで実際に採取したclaude_code.user_promptイベントです。前掲のRun Aの例(claude_code.hook_execution_start)と見比べると、user.email・organization.id・user.account_uuid・user.account_idが属性に一切現れていません。
LogRecord #0
Body: Str(claude_code.user_prompt)
Attributes:
-> user.id: Str(<USER_ID_HASH_C>)
-> session.id: Str(<SESSION_ID>)
-> terminal.type: Str(xterm)
-> event.name: Str(user_prompt)
-> prompt.id: Str(<PROMPT_ID>)
-> prompt_length: Str(33)
-> prompt: Str(Reply with exactly one word: pong)
-> message.uuid: Str(<MESSAGE_UUID>)
Run Cではuser.email・organization.id・user.account_uuid・user.account_idが一切出力されませんでした。残ったuser.idもRun A/Bとは異なるハッシュ値で、Anthropicアカウントに紐づく識別子ではなく、インストール単位で振られる匿名IDだと考えられます。
/loginを一度でも実行したことのある端末なら、setup-tokenでの利用であってもテレメトリから利用者を特定できます。逆に、/loginを一度も実行していない真にトークンのみの環境(典型的にはCI/CD)では、テレメトリもclaude auth statusと同じく匿名化されます。
おわりに
ただし、OTelのログをトークン利用の判別材料にする方法には限界があります。OTelは既定で無効で、有効にするにはCLAUDE_CODE_ENABLE_TELEMETRY等の環境変数か、組織側のmanaged settings経由での配信が必要です(Configure server-managed settings - Claude Code Docs)。
組織が管理している端末ならmanaged settingsでテレメトリを強制できますが、私用端末からの利用や、トークンが流出して管理外の端末で使われた場合は、そもそもOTelが有効になっていないことがあります。その場合はログ自体が残らないため、判別のしようがありません。








