😽
複数GitHubリポジトリの Claude Code のシークレットを一括更新するPythonスクリプト
はじめに
Claude CodeやGitHub Actionsを複数のリポジトリで使っている場合、認証情報(シークレット)の更新が大変ですよね。10個のリポジトリがあれば、10回同じ作業を繰り返す必要があります。
この記事では、Pythonスクリプト1つで全リポジトリのシークレットを一括更新する方法を紹介します。
解決したい課題
- 各リポジトリのSettings → Secretsを手動で更新するのが面倒
- コピペミスや更新漏れが発生しやすい
- リポジトリが増えるほど作業時間が増える
- 認証トークンの有効期限切れで定期的な更新が必要
実装アイデア
GitHub APIを使って、以下の流れで自動化します:
- 更新したいシークレット情報をJSONファイルに記載
- Pythonスクリプトで対象リポジトリを指定
- GitHub APIで各リポジトリのシークレットを一括更新
必要なファイル構成
project/
├── update_claude_credentials.py # メインスクリプト
├── requirements.txt # 依存ライブラリ
├── claude_credentials.json # 認証情報(.gitignoreに追加!)
└── .gitignore # 認証情報をコミットしないように
実装方法
1. 必要なライブラリをインストール
requirements.txt
:
PyGithub>=2.0.0
python-dotenv>=1.0.0
click>=8.0.0
pip install -r requirements.txt
2. 認証情報ファイルを作成
claude_credentials.json
:
{
"claudeAiOauth": {
"accessToken": "xxxxxxxxxxxxxxxxxxxx",
"refreshToken": "xxxxxxxxxxxxxxxxxxx",
"expiresAt": 1749360577082,
"scopes": ["user:inference", "user:profile"],
"isMax": true
}
}
3. メインスクリプトの実装
update_claude_credentials.py
:
#!/usr/bin/env python3
import json
import sys
import os
from datetime import datetime
from typing import List, Dict, Any
try:
import click
from github import Github
from github.Repository import Repository
except ImportError:
print("Error: Required packages not installed.")
print("Please run: pip install PyGithub click python-dotenv")
sys.exit(1)
@click.command()
@click.argument('credentials_file', type=click.Path(exists=True))
@click.option('--token', envvar='GITHUB_TOKEN', required=True,
help='GitHub Personal Access Token (can be set via GITHUB_TOKEN env var)')
@click.option('--repos', multiple=True,
help='Specific repositories to update (format: owner/repo)')
@click.option('--all', is_flag=True,
help='Update all repositories you have admin access to')
@click.option('--dry-run', is_flag=True,
help='Show what would be updated without making changes')
@click.option('--org', help='Organization name to filter repositories')
def update_secrets(credentials_file: str, token: str, repos: tuple, all: bool,
dry_run: bool, org: str) -> None:
"""
Update Claude credentials across multiple GitHub repositories.
CREDENTIALS_FILE: Path to JSON file containing Claude OAuth credentials
"""
# Initialize GitHub client
try:
g = Github(token)
user = g.get_user()
click.echo(f"Authenticated as: {user.login}")
except Exception as e:
click.echo(f"Error: Failed to authenticate with GitHub: {str(e)}", err=True)
sys.exit(1)
# Load credentials
try:
with open(credentials_file, 'r') as f:
data = json.load(f)
except json.JSONDecodeError as e:
click.echo(f"Error: Invalid JSON in {credentials_file}: {str(e)}", err=True)
sys.exit(1)
except Exception as e:
click.echo(f"Error: Failed to read {credentials_file}: {str(e)}", err=True)
sys.exit(1)
# Extract secrets based on format
if 'claudeAiOauth' in data:
# Claude OAuth format
oauth_data = data['claudeAiOauth']
secrets_to_update = {
'CLAUDE_ACCESS_TOKEN': oauth_data.get('accessToken', ''),
'CLAUDE_REFRESH_TOKEN': oauth_data.get('refreshToken', ''),
'CLAUDE_EXPIRES_AT': str(oauth_data.get('expiresAt', ''))
}
# Display token expiration
if oauth_data.get('expiresAt'):
try:
expires_at = datetime.fromtimestamp(oauth_data['expiresAt'] / 1000)
click.echo(f"Token expires at: {expires_at}")
# Warn if token is expired or expiring soon
now = datetime.now()
if expires_at < now:
click.echo("⚠️ Warning: Token has already expired!", err=True)
elif (expires_at - now).days < 7:
click.echo(f"⚠️ Warning: Token expires in {(expires_at - now).days} days!")
except:
pass
# Display scope information
if oauth_data.get('scopes'):
click.echo(f"Scopes: {', '.join(oauth_data['scopes'])}")
if oauth_data.get('isMax'):
click.echo("Max plan: Yes")
elif 'apiKeys' in data:
# Generic API keys format
secrets_to_update = data['apiKeys']
else:
# Flat format (backward compatibility)
secrets_to_update = {k: v for k, v in data.items()
if k.startswith('CLAUDE_') or k in ['CLAUDE_ACCESS_TOKEN',
'CLAUDE_REFRESH_TOKEN',
'CLAUDE_EXPIRES_AT']}
if not secrets_to_update:
click.echo("Error: No secrets found to update", err=True)
sys.exit(1)
click.echo(f"\nSecrets to update: {', '.join(secrets_to_update.keys())}")
# Determine target repositories
target_repos: List[Repository] = []
if all:
# Get all repos with admin access
if org:
try:
organization = g.get_organization(org)
all_repos = organization.get_repos()
click.echo(f"Checking repositories in organization: {org}")
except Exception as e:
click.echo(f"Error: Failed to access organization {org}: {str(e)}", err=True)
sys.exit(1)
else:
all_repos = user.get_repos()
target_repos = [repo for repo in all_repos if repo.permissions.admin]
click.echo(f"Found {len(target_repos)} repositories with admin access")
elif repos:
# Get specific repositories
for repo_name in repos:
try:
repo = g.get_repo(repo_name)
if not repo.permissions.admin:
click.echo(f"Warning: No admin access to {repo_name}, skipping", err=True)
continue
target_repos.append(repo)
except Exception as e:
click.echo(f"Error: Failed to access {repo_name}: {str(e)}", err=True)
click.echo(f"Found {len(target_repos)} accessible repositories")
else:
click.echo("Error: Please specify repositories with --repos or use --all", err=True)
sys.exit(1)
if not target_repos:
click.echo("No repositories to update", err=True)
sys.exit(1)
# Confirm before proceeding
if not dry_run:
click.echo(f"\nAbout to update {len(target_repos)} repositories.")
if not click.confirm("Do you want to continue?"):
click.echo("Aborted.")
sys.exit(0)
# Update repositories
success_count = 0
failed_repos = []
for repo in target_repos:
click.echo(f"\n{'[DRY RUN] ' if dry_run else ''}Updating: {repo.full_name}")
if dry_run:
for secret_name in secrets_to_update.keys():
click.echo(f" Would update: {secret_name}")
success_count += 1
continue
# Actually update secrets
repo_success = True
for secret_name, secret_value in secrets_to_update.items():
try:
repo.create_secret(secret_name, str(secret_value))
click.echo(f" ✓ {secret_name}")
except Exception as e:
click.echo(f" ✗ {secret_name}: {str(e)}", err=True)
repo_success = False
if repo_success:
success_count += 1
else:
failed_repos.append(repo.full_name)
# Summary
click.echo("\n" + "="*50)
click.echo(f"{'DRY RUN ' if dry_run else ''}Summary:")
click.echo(f" Total repositories: {len(target_repos)}")
click.echo(f" Successfully updated: {success_count}")
click.echo(f" Failed: {len(failed_repos)}")
if failed_repos:
click.echo(f"\nFailed repositories:")
for repo_name in failed_repos:
click.echo(f" - {repo_name}")
if dry_run:
click.echo("\nThis was a dry run. No changes were made.")
else:
click.echo(f"\n✅ Done! Updated {success_count} repositories.")
if __name__ == '__main__':
update_secrets()
4. .gitignoreの設定
.gitignore
:
# 認証情報を絶対にコミットしない
claude_credentials.json
*.credentials.json
.env
使い方
GitHub Personal Access Tokenの準備
- GitHub Settings → Developer settings → Personal access tokens
- 「Generate new token」をクリック
- 必要な権限:
repo
(リポジトリへのフルアクセス) - トークンを環境変数に設定:
export GITHUB_TOKEN="your-github-token"
実行例
特定のリポジトリを更新
python update_claude_credentials.py --repos project1 project2 project3
管理者権限を持つ全リポジトリを更新
python update_claude_credentials.py --all
別の認証情報ファイルを使用
python update_claude_credentials.py --credentials-file prod_credentials.json --all
実行結果の例
Found 15 repositories with admin access
Token expires at: 2025-02-07 12:09:37
Updating secrets for: my-blog
✓ CLAUDE_ACCESS_TOKEN
✓ CLAUDE_REFRESH_TOKEN
✓ CLAUDE_EXPIRES_AT
Updating secrets for: api-server
✓ CLAUDE_ACCESS_TOKEN
✓ CLAUDE_REFRESH_TOKEN
✓ CLAUDE_EXPIRES_AT
...
Done!
応用例
1. 他のシークレットも一括更新
api_credentials.json
:
{
"apiKeys": {
"OPENAI_API_KEY": "sk-...",
"SLACK_WEBHOOK_URL": "https://hooks.slack.com/...",
"DATABASE_URL": "postgresql://..."
}
}
対応するスクリプトの修正:
# JSONファイルの形式に応じて処理を分岐
if 'claudeAiOauth' in data:
# Claude認証情報の処理
oauth_data = data['claudeAiOauth']
secrets_to_update = {
'CLAUDE_ACCESS_TOKEN': oauth_data['accessToken'],
'CLAUDE_REFRESH_TOKEN': oauth_data['refreshToken'],
'CLAUDE_EXPIRES_AT': str(oauth_data['expiresAt'])
}
elif 'apiKeys' in data:
# その他のAPI キーの処理
secrets_to_update = data['apiKeys']
else:
# 従来のフラット形式
secrets_to_update = data
2. 組織のリポジトリも対象に
# 組織のリポジトリを取得
org = g.get_organization('my-organization')
target_repos = [repo for repo in org.get_repos()
if repo.permissions.admin]
3. ドライランモード追加
@click.option('--dry-run', is_flag=True, help='Show what would be updated')
def update_secrets(token, repos, all, credentials_file, dry_run):
if dry_run:
click.echo(f"Would update: {secret_name}")
else:
repo.create_secret(secret_name, secret_value)
メリット
- 時間短縮: 10リポジトリでも1コマンドで完了
- ミス防止: 手動コピペによるエラーがゼロに
- 定期更新が簡単: cronやGitHub Actionsで自動化可能
セキュリティ注意事項
- 認証情報ファイルは絶対にGitにコミットしない
- GitHub Personal Access Tokenは最小権限で作成
- スクリプト実行後は認証情報ファイルを削除
Claude認証情報の取得方法
Claude Codeの認証情報は以下の手順で取得できます:
- Claude Codeアプリケーションを開く
- 開発者ツールを開く(F12 または Cmd+Option+I)
- Application/Storage → Local Storage →
https://claude.ai
-
claude_auth
キーの値をコピー - JSONファイルとして保存
まとめ
このシンプルなPythonスクリプトで、複数リポジトリの管理が劇的に楽になります。特にClaude Codeのような定期的にトークン更新が必要なサービスを使っている場合、このスクリプトは必須ツールになるでしょう。
認証情報の形式(claudeAiOauth
)に注意して、ぜひ自分の環境に合わせてカスタマイズして使ってみてください!
参考リンク:
Discussion