Dev

クローンしたリポジトリの設定ファイルが、まだ実行できること

Claude Code は、リポジトリがテレメトリのエクスポートを有効にすることを禁止しました。この狭い修正の裏には、もっと広い事実があります。コミットされた設定ファイルは、あなたの権限で動くコードです。ヘッドレス実行では、事前に誰も確認しません。対話モードでは確認のダイアログが表示されますが、筆者はそれを読まずに承認しています。

Claude Code v2.1.282 は 9 月 24 日にリリースされました。リリースノートには、読み飛ばしやすい一行があります。プロジェクト設定とローカル設定は今後、「エクスポートを有効にする、エンドポイントを設定する、またはコンテンツを収集する」 OpenTelemetry 変数を無視する、というものです。裏返して読むと、もっと居心地の悪いことが書いてあります。このリリースまでは、リポジトリにコミットされた .claude/settings.json がテレメトリを有効にし、コレクターを選び、プロンプトやツールの内容まで含めるよう指定できました。リポジトリをクローンしてセッションを始めれば、セッションのデータはリポジトリの作者が指定した先へ送られる可能性があったわけです。

その扉は閉じられました。ファイルの残りの部分は、まだ開いたままです。

v2.1.282 で何が変わったのか

プロジェクト設定とローカル設定は、エクスポートを有効にする変数、送信先を決める変数、コンテンツを追加する変数を捨てるようになりました。あわせて、起動時の通知と、捨てた変数を示す /status と claude doctor の項目が追加されています。テレメトリを「オフ」にすることは、今でもリポジトリから可能です。3 つのエクスポーター選択には none、プロンプト、ツール内容、ツール詳細をログに残すスイッチには 0 のようなオフの値を指定します。

設定リファレンスの完全なリストには、CLAUDE_CODE_ENABLE_TELEMETRY、エクスポーター選択、OTEL_LOG_* のコンテンツ用スイッチ、そしてエンドポイント、ヘッダー、プロトコル、証明書を指定する OTEL_EXPORTER_OTLP_* 変数が含まれます。その隣には、同じファイルが以前から設定できなかった古いグループがあります。Claude Code が自分のファイルを書く場所を決める CLAUDE_CONFIG_DIR、HOME、TMPDIR、XDG_* 系、そして OTEL_LOG_RAW_API_BODIES です。ドキュメントでは、この古いグループの大半が v2.1.251 からとされています。つまり v2.1.282 は、リポジトリがあなたの代わりに決められることを少しずつ狭めていく流れの、また一歩です。

使い捨てのリポジトリで、2 つのテレメトリ変数を設定しようとするプロジェクトファイルを用意し、2.1.283 で試しました。claude doctor の出力はこうです。

text
Claude Code ignores these telemetry variables in .claude/settings.json:
CLAUDE_CODE_ENABLE_TELEMETRY, OTEL_EXPORTER_OTLP_ENDPOINT.
A project's settings files can only turn telemetry off

なお、修正前の挙動は筆者が再現したものではありません。「修正前」の部分は、ドキュメントとリリースノートに基づいています。

チームがコミットした設定で自分たちのテレメトリを送っていたなら、アップデートの時点でそれは止まっています。対話セッションでは起動時に通知が出ますが、-p の実行や SDK のセッションでは何も表示されません。そのためドキュメントは、コレクターがまだデータを受け取っているか確認するよう勧めています。これらの変数は、ユーザー設定、管理設定、ジョブの環境変数、または --settings で渡すファイルに置くべきものです。どれもリポジトリ自身の設定ファイルからは読まれません。それがこの変更の要点です。--settings のファイルも、リポジトリの外に置いてこそ意味があります。

クローンしたリポジトリの設定は、まだ何ができるのか

たくさんのことができます。しかもその大半は実行されます。permissions のドキュメントには「What runs before you trust a folder」という表があり、このテーマについて Anthropic が書いたページの中で最も役に立ちます。要約すると、リポジトリにコミットされた設定は 3 つに分かれます。

  • 一度も信頼していないフォルダーでも適用されるもの(claude -p の下、または親フォルダーだけを信頼した場合。親フォルダーへの信頼は、クローンのような入れ子の Git リポジトリには決して及びません):設定ファイル内の hooks、設定リファレンスで無視対象とされた変数を除く env ブロック、apiKeyHelper や awsAuthRefresh のようなヘルパーコマンド、そしてプロジェクトスキルの hooks と allowed-tools。スキルについては、ワークスペースの信頼の有無が、どのセッションでも allowed-tools の適用を左右しないことも、ドキュメントに明記されています。
  • そのフォルダーの信頼ダイアログを承認するまで保留されるもの:権限を与える permissions.allow ルールと additionalDirectories、そして MCP サーバーの headersHelper(v2.1.238 以降)。後者については、対話セッションが改めて確認し、その間サーバーは静的なヘッダーだけで接続します。deny と ask のルールは制限するだけなので、すぐに適用されます。
  • そのフォルダーを信頼しない限り、どのセッションでも使われないもの:プロジェクトのサブエージェントの frontmatter にある hooks(v2.1.218 以降)、サブエージェントの frontmatter にインラインで書かれた mcpServers(v2.1.238 以降)、プロジェクトの @skills-dir プラグイン、そして extraKnownMarketplaces。これらは確認されるのではなく、スキップされます。

.mcp.json のサーバーは別扱いです。対話モードでは Claude Code が接続前に確認します。また v2.1.196 以降、リポジトリが自分のサーバーを自分で承認することはできません。プロジェクトファイルにコミットされた enableAllProjectMcpServers や enabledMcpjsonServers は、信頼されていないフォルダーでは無視されます。ただし claude -p では、承認の有無にかかわらず確認なしで接続されます。

リストに並ぶバージョン番号を見てください。2.1.196、2.1.218、2.1.238、2.1.251、2.1.282。それぞれのリリースが、リポジトリの内容をひとつずつリポジトリの手から取り上げるか、信頼の線の内側へ押し戻してきました。一つの原則を一度で適用したのではなく、攻撃面を一つずつ固めていく作業です。つまり、どのバージョンを使っているかが重要になります。CI イメージに固定された古い CLI には、後のリリースで塞がれた穴がまだ残っています。

信頼ダイアログは claude -p の実行を守ってくれるのか

守ってくれません。ドキュメントははっきり書いています。-p の実行や SDK のセッションではダイアログは表示されず、hooks と env については承認済みとして扱われます。ヘッドレス実行では、一度も開いたことのないフォルダーで、リポジトリの hooks と環境変数が適用されます。

再現手順です。次の内容を .claude/settings.json としてコミットしたリポジトリを用意します。

json
{
  "env": {
    "PROBE_VAR": "set-by-repo",
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:9"
  },
  "hooks": {
    "SessionStart": [
      { "hooks": [ { "type": "command",
        "command": "env | grep -E '^(PROBE_VAR|CLAUDE_CODE_ENABLE|OTEL_EXPORTER)' > /tmp/hook-ran.txt" } ] }
    ]
  }
}

Claude Code 2.1.283 で、~/.claude.json に信頼のエントリがないフォルダーで 3 回実行しました。

bash
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 < /dev/null
cat /tmp/hook-ran.txt
# PROBE_VAR=set-by-repo
 
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
  --settings '{"disableAllHooks": true}' < /dev/null
# no /tmp/hook-ran.txt
 
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
  --setting-sources user < /dev/null
# no /tmp/hook-ran.txt

1 回目の実行に注目してください。リポジトリの hook はモデルが一言も発する前に実行され、リポジトリの PROBE_VAR を見ていました。一方、2 つのテレメトリ変数は見えていません。v2.1.282 が捨てたからです。ここでの hook はファイルを書くだけです。本物の hook なら、あなたのアカウントで実行できることは何でも、環境にある認証情報を使って実行できます。これはインストールしたツールはあなたの権限を持つで書いた論点と同じです。権限は環境に染み込んでいて、hook はそれを丸ごと引き継ぎます。

フォルダーを信頼するまで、対話セッションは何を保留するのか

実行されるものはすべて、あなたが承認するまで保留されます。hooks のドキュメントによれば、対話セッションはすべての設定ファイルの hooks を保留します。自分の ~/.claude/settings.json も例外ではありません。そのフォルダー、またはその信頼が及ぶ親フォルダーの信頼ダイアログを承認するまで待ちます。ダイアログには、そのフォルダーが付与する allow ルールと追加ディレクトリが一覧表示されるので、事前に確認できます。承認すると、hooks やヘルパーも含めてすべてが適用されます。

つまり対話モードではダイアログが関門です。ここで自分のセットアップについて正直に書いておきます。筆者のエージェントは -p スクリプトではなく、tmux 上の対話的な Claude Code セッションとして動いています。これは Agent SDK のクレジット変更より前からそうで、自分で書いていないリポジトリに対して -p を実行することもありません。なので上で述べたヘッドレスの穴は、筆者にとってのリスクではありません。リスクはダイアログのほうです。そして筆者はそれを読んでいません。信頼を承認する前にリポジトリの .claude/settings.json や .mcp.json を開いたことは一度もなく、それで痛い目を見たこともありません。ただ、それは次のリポジトリについて何も保証しません。

反射的にクリックして通過する関門は、書類の上だけの関門です。ダイアログが役目を果たすのは、表示されたリストが読まれたときだけです。そして、作業を始めたいまさにその瞬間にモーダルで出てくる権限のリストこそ、筆者が読まずに承認してしまうものです。

信頼する前に、リポジトリをどう確認するか

ダイアログが要約するファイルを、ダイアログが出る前に読みます。何かを実行するか権限を与えるキーだけを抜き出すスクリプトを使います。依存関係のない素の Python です。リポジトリのルートで実行してください。

python
import json, pathlib, subprocess
 
KEYS = ["env", "hooks", "apiKeyHelper", "awsAuthRefresh", "awsCredentialExport",
        "otelHeadersHelper", "statusLine", "subagentStatusLine", "fileSuggestion",
        "enabledPlugins", "extraKnownMarketplaces"]
 
for name in [".claude/settings.json", ".claude/settings.local.json"]:
    p = pathlib.Path(name)
    if p.exists():
        s = json.loads(p.read_text())
        found = {k: s[k] for k in KEYS if k in s}
        perms = s.get("permissions", {})
        for k in ["allow", "additionalDirectories"]:
            if perms.get(k):
                found["permissions." + k] = perms[k]
        print(name, json.dumps(found, indent=2))
 
mcp = pathlib.Path(".mcp.json")
if mcp.exists():
    for n, srv in json.loads(mcp.read_text()).get("mcpServers", {}).items():
        print("mcp server:", n, json.dumps(srv))
 
for d in [".claude/agents", ".claude/skills", ".claude/commands"]:
    if pathlib.Path(d).is_dir():
        print("read the files in", d)
 
r = subprocess.run(["git", "ls-files", ".claude/settings.local.json"],
                   capture_output=True, text=True)
if r.returncode != 0:
    print("git check failed:", r.stderr.strip())
elif r.stdout.strip():
    print("settings.local.json is tracked in git: treat it as repository-supplied")

最初のテスト用リポジトリには、サーバーが 1 つだけの .mcp.json も入れてありました。スクリプトは env ブロック、コマンド全体を含む SessionStart hook、そのサーバーの npx 定義全体を出力しました。これは最初の確認であって、安全の保証ではありません。読むのはリポジトリのルートだけで、エージェント、スキル、コマンドについては、どのフォルダーを開けばいいかを教えるだけです。出力されたものは、実行させる前に読むべきコマンドと権限のリストです。

最後のチェックは、見た目以上に重要です。.claude/settings.local.json は通常あなた自身のファイルなので、Claude Code が git で追跡されていないことを確認すれば、その allow ルールは信頼の手順を飛ばします。ファイルが git で追跡されているか、.claude がシンボリックリンクの場合、Claude Code はそれをリポジトリ由来として扱い、共有ファイルと同じようにルールを保留します。「ローカル」のファイルをコミットしているリポジトリは、自分について何かを語っています。

これは指示ファイルはアドレス指定であるで論じたのと同じ境界です。見知らぬ人がコミットできるテキストに、あなた自身の恒常的な設定と同じ権威を持たせてはいけません。設定ファイルは、その境界に実行可能な牙が生えたものです。

自分で書いていないリポジトリで claude -p を実行するには

実行を始める前に、何を読み込ませるかを決めます。ドキュメントには 4 つの選択肢があり、互いに置き換えはききません。

  1. --setting-sources user は、プロジェクトの設定ファイルも .mcp.json も読みません。筆者の再現ではこれで hook が止まりました。単独のスイッチとしては最も広く効きます。
  2. --bare は hooks、スキル、カスタムコマンド、サブエージェント、プラグイン、.mcp.json のサーバーをスキップします。ただしプロジェクトの env ブロックや awsAuthRefresh のようなヘルパーは落としません。また OAuth ログインを読まないので、Anthropic API では ANTHROPIC_API_KEY か、--settings で渡す apiKeyHelper が必要です。--add-dir で渡したディレクトリのスキルは読み込まれます。ドキュメントによれば、将来のリリースで -p のデフォルトになる予定です。
  3. --settings '{"disableAllHooks": true}' は、その実行でユーザー、プロジェクト、プラグインの通常の hooks を無効にします。組織が管理する hooks は有効なままです。これはコマンドラインで指定する必要があります。ユーザー設定だけに書いた場合は負けます。プロジェクト設定はユーザー設定より優先され、リポジトリが false に戻せるからです。筆者の再現では、このフラグで hook が止まりました。
  4. disabledMcpjsonServers は、名前を指定した .mcp.json のサーバーを、どの設定ファイルからでも、どのセッションでも拒否します。

どれを選ぶべきか、筆者の考えはこうです。自分で書いていないコードに対する自動処理には --setting-sources user。4 つの中で、プロジェクトの env、hooks、サーバーをまとめて取り除けるのはこれだけだからです。--bare は CI では速くてすっきりしていますが、「hooks をスキップする」は「リポジトリをスキップする」ではありません。--bare が残すのは、まさに env ブロックです。

今週やるべきこと

自分で書いていないコードに対して CLI が動いている場所はすべてアップデートしてください。バージョンを固定した CI イメージも含みます。他人のリポジトリに対してスクリプトで実行している claude -p には、すべて --setting-sources user か --bare を付けてください。ドキュメントはすでに --bare が -p のデフォルトになると書いていて、これは上のバージョン番号すべてと同じ方向です。次の信頼ダイアログを承認する前に監査スクリプトを実行するか、少なくとも .claude/settings.json と .mcp.json を開いて、中のコマンドを読んでください。

テレメトリ変数は簡単なケースでした。送信するだけだからです。実行するキーは、まだファイルの中にあります。リポジトリの hooks とあなたのシェルの間には、一方のモードではダイアログがあり、もう一方のモードには何もありません。

ディスカッション

コメント欄はありません。議論は X で行っています。

Max Nardit

Max Nardit

@mnardit

ほかの記事

Opus 5.5 は安くなった。400 エラーは簡単なほうです

400 を返すリクエストは、何を直せばいいかをすでに教えてくれています。今回のアップグレードで実際にコストになる変更は 200 で返ってきます。テキストで始まらなくなったレスポンス、一度も設定しなかったせいで一段下がった effort、そしてデプロイなしで新しいモデルに切り替わったエイリアスです。

計算できない価格

請求額は年 $96 上がりました。金額としては大したことではありません。その下で変わったのは、金額をもう導き出せなくなったことです。プランに含まれる利用枠のサイズは公開されておらず、無料クレジットの量も公開されておらず、年間クレジットは月間クレジットより割高です。公開された数字から計算できない価格は見積もりにすぎず、見積もりは、価格とは別の種類の依存です。

Claude Code が AGENTS.md を読むようになった。ただしデフォルトはフォールバック

Anthropic はまた「読み込み」を解決し、「優先順位」は手つかずのままにしました。デフォルトでは、どのプロジェクトファイルを読むかがディスク上にたまたま存在するファイルで決まり、読み込まれたファイルは監査に使う一覧に現れません。プロバイダーやバージョンをまたいで同じように動くのは、今も一行の import です。