CLAUDE LABEN
OPUS5 — Claude Code v2.1.219でClaude Opus 5が既定のOpusモデルになりました。1Mコンテキストに対応し、fastモードは100万トークンあたり入力$10・出力$50ですSONNET5 — Claude Sonnet 5が全世界のFree・Proの既定モデルになりました。導入価格は100万トークンあたり入力$2・出力$10で、8月31日までですSANDBOX — Claude Codeにsandbox.network.strictAllowlistが追加され、サンドボックス実行で許可リストにないホストへの通信を拒否できますSTREAM — stream-json出力が入れ子のサブエージェント転送に対応し、多段のエージェント連携でも内側のやり取りを追えますSKILL — Messages APIとManaged Agentsの最新リファレンスを8言語で提供するオープンソースのClaude API skillがClaude Codeに同梱されましたVOICE — ボイスモードがOpus・Sonnet・Haikuで動作し、GmailやSlackなど接続済みツールに届き、対応言語も増えましたOPUS5 — Claude Code v2.1.219でClaude Opus 5が既定のOpusモデルになりました。1Mコンテキストに対応し、fastモードは100万トークンあたり入力$10・出力$50ですSONNET5 — Claude Sonnet 5が全世界のFree・Proの既定モデルになりました。導入価格は100万トークンあたり入力$2・出力$10で、8月31日までですSANDBOX — Claude Codeにsandbox.network.strictAllowlistが追加され、サンドボックス実行で許可リストにないホストへの通信を拒否できますSTREAM — stream-json出力が入れ子のサブエージェント転送に対応し、多段のエージェント連携でも内側のやり取りを追えますSKILL — Messages APIとManaged Agentsの最新リファレンスを8言語で提供するオープンソースのClaude API skillがClaude Codeに同梱されましたVOICE — ボイスモードがOpus・Sonnet・Haikuで動作し、GmailやSlackなど接続済みツールに届き、対応言語も増えました
記事一覧/Claude Code
Claude Code/2026-05-03中級

git pushが詰まるVM環境でGitHub REST APIを使ってcommit/pushする方法

index.lockや所有権エラーでgit CLIが動かないVM・サンドボックス環境で、GitHub REST API(blobs→trees→commits→refs)を使ってファイルをpushする実践ガイドです。

GitHub REST APIClaude Code203git16VM2サンドボックス6push3

Claude Code のスケジュールタスクやサンドボックス環境で作業していると、ある日突然こんなエラーに遭遇することがあります。

fatal: detected dubious ownership in repository at '/tmp/repos/mysite'
error: could not lock config file .git/config: Permission denied
Another git process seems to be running in this repository

index.lock が残っている、リポジトリの所有者が nobody になっている、/tmp への書き込み権限がない——こういったケースはVMやコンテナ環境では珍しくありません。個人開発で複数サイトの更新を自動化している私自身、Claude Code のエージェント実行中に何度もこの問題で作業が止まりました。

そこで行き着いたのが、git CLIを一切使わず、GitHub REST APIだけでcommit/pushする方法です。

なぜGitHub REST APIなのか

git CLIは「Gitリポジトリへの直接アクセス」を前提にしています。ファイルシステムの権限、プロセスロック、所有者チェック——これらすべてが正常でないと動きません。

一方、GitHub REST APIはHTTPリクエストです。リポジトリのローカルコピーが壊れていても、権限が変になっていても、GitHubサーバー上のデータに直接操作できます

git CLI → ローカル.git → GitHub(権限エラーで詰まる)
REST API → GitHub(ローカル状態に関係なく動く)

基本的な仕組み:4ステップのフロー

GitHub REST APIでコミットを作るには、以下の順番で操作します。

1. blob作成  → ファイルの内容をGitHubに登録
2. tree作成  → blobを束ねてディレクトリ構造を作る
3. commit作成 → treeにメッセージを付けてコミットを作る
4. ref更新   → HEADが指すブランチを新コミットに進める

一見面倒ですが、各ステップは独立したAPIコールなので、エラーが起きても再試行しやすいのが利点です。

実装例:シェルスクリプト版

以下は、bashとcurlだけで実装したサンプルです。Claude Code のスケジュールタスクやCI環境でそのまま使えます。

#!/bin/bash
set -e
 
GITHUB_TOKEN="YOUR_GITHUB_TOKEN"
OWNER="your-username"
REPO="your-repo"
BRANCH="main"
COMMIT_MSG="Add: new article (JA+EN)"
 
# ── Step 1: 最新のHEAD SHAを取得 ──────────────────
HEAD_SHA=$(curl -sf \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/heads/${BRANCH}" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")
 
BASE_TREE=$(curl -sf \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/commits/${HEAD_SHA}" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['tree']['sha'])")
 
echo "HEAD: ${HEAD_SHA}"
echo "BASE_TREE: ${BASE_TREE}"
 
# ── Step 2: blobを作成(変更ファイルごとに実行) ──
FILE_PATH="content/articles/ja/claude-code/my-article.mdx"
FILE_CONTENT=$(cat "${FILE_PATH}")
 
BLOB_SHA=$(curl -sf \
  -X POST \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/blobs" \
  -d "$(python3 -c "
import sys, json
content = open('${FILE_PATH}', 'r').read()
print(json.dumps({'content': content, 'encoding': 'utf-8'}))
")" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha'])")
 
echo "BLOB: ${BLOB_SHA}"
 
# ── Step 3: treeを作成 ──────────────────────────────
TREE_SHA=$(curl -sf \
  -X POST \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/trees" \
  -d "$(python3 -c "
import json
tree = {
    'base_tree': '${BASE_TREE}',
    'tree': [{
        'path': '${FILE_PATH}',
        'mode': '100644',
        'type': 'blob',
        'sha': '${BLOB_SHA}'
    }]
}
print(json.dumps(tree))
")" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha'])")
 
echo "TREE: ${TREE_SHA}"
 
# ── Step 4: commitを作成 ───────────────────────────
COMMIT_SHA=$(curl -sf \
  -X POST \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/commits" \
  -d "$(python3 -c "
import json
commit = {
    'message': '${COMMIT_MSG}',
    'tree': '${TREE_SHA}',
    'parents': ['${HEAD_SHA}']
}
print(json.dumps(commit))
")" | python3 -c "import sys,json; print(json.load(sys.stdin)['sha'])")
 
echo "COMMIT: ${COMMIT_SHA}"
 
# ── Step 5: refを更新(ブランチを進める) ─────────
curl -sf \
  -X PATCH \
  -H "Authorization: token ${GITHUB_TOKEN}" \
  -H "Content-Type: application/json" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/heads/${BRANCH}" \
  -d "{\"sha\": \"${COMMIT_SHA}\"}" > /dev/null
 
echo "✅ Push完了: ${COMMIT_SHA}"

複数ファイルを1コミットにまとめる

日英セットの記事を1コミットにまとめたい場合は、Step 3のtree作成時に複数ファイルを配列に入れます。

# 複数ファイルをまとめてtreeに追加する例
python3 -c "
import json
 
# JA版のblob SHAとEN版のblob SHAを事前に取得しておく
tree_items = [
    {
        'path': 'content/articles/ja/claude-code/my-article.mdx',
        'mode': '100644',
        'type': 'blob',
        'sha': '${BLOB_JA_SHA}'
    },
    {
        'path': 'content/articles/en/claude-code/my-article.mdx',
        'mode': '100644',
        'type': 'blob',
        'sha': '${BLOB_EN_SHA}'
    }
]
 
payload = {
    'base_tree': '${BASE_TREE}',
    'tree': tree_items
}
print(json.dumps(payload))
"

blobは並行して作成できるので、ファイル数が増えても待ち時間は最小限に抑えられます。

base_treeを省略するとリポジトリが空になる

REST APIで最も怖い落とし穴が、Step 3のtree作成で base_tree を指定し忘れることです。

base_tree は「既存のディレクトリ構造を土台にする」という指定です。これを省くと、GitHubは あなたがtreeに入れたファイルだけ で新しいツリーを作ります。つまり、記事を1本追加したつもりが、リポジトリの他のファイルを全て削除するコミットになってしまいます。

私はこの挙動をテスト用リポジトリで一度だけ再現させて、肝を冷やしました。幸い本番ではありませんでしたが、git push --force でディレクトリごと消すのと同じ威力があります。

# ❌ base_tree なし → 他のファイルが全て消える
tree = {'tree': [{'path': FILE, 'mode': '100644', 'type': 'blob', 'sha': BLOB}]}
 
# ✅ base_tree あり → 既存ツリーに差分を重ねる
tree = {'base_tree': BASE_TREE, 'tree': [{'path': FILE, ...}]}

base_tree には、Step 1で取得した最新コミットのtree SHAを必ず渡してください。差分コミットの安全装置だと考えると分かりやすいと思います。

ファイルの削除・リネームもAPIでできる

treeのエントリで shanull にすると、そのパスのファイルを削除できます。記事を410で消すときなど、追加だけでなく削除も同じフローで扱えます。

tree_items = [
    # 既存ファイルを削除
    {'path': 'content/articles/ja/claude-code/old.mdx',
     'mode': '100644', 'type': 'blob', 'sha': None},
    # 新しいパスに同じ内容を追加(= リネーム)
    {'path': 'content/articles/ja/claude-code/new.mdx',
     'mode': '100644', 'type': 'blob', 'sha': BLOB_SHA},
]

リネームは「旧パスを削除 + 新パスを追加」を1つのtreeにまとめるだけです。git mv に相当する専用APIはありませんが、この組み合わせで過不足なく実現できます。

エラーハンドリングのポイント

REST APIを使う際に実際に遭遇しやすいエラーと対処法をまとめます。

422 Unprocessable Entity(blob作成時)

バイナリファイルやBase64エンコードが必要な場合に発生します。テキストファイルなら encoding: "utf-8" で問題ありません。

409 Conflict(ref更新時)

別の誰かが先にpushしている場合です。git pull --rebase に相当する処理として、最新のHEAD SHAを再取得してからやり直します。

401 Unauthorized

トークンの権限不足か期限切れです。GitHub Personal Access Tokenに repo スコープが付いているか確認してください。

git CLIと組み合わせるハイブリッド戦略

VM環境でも /tmp 以外の場所($HOME/repos/ など)にリポジトリをcloneできる場合は、まずgit CLIを試みて、失敗したらREST APIにフォールバックするハイブリッド戦略も有効です。

push_article() {
  local repo_path="$1"
  
  # git CLIを試みる
  if cd "$repo_path" && git push origin main 2>/dev/null; then
    echo "✅ git push成功"
    return 0
  fi
  
  echo "⚠️ git push失敗 → REST APIにフォールバック"
  # REST API pushの処理を実行...
}

私はこのパターンをClaudeのスケジュールタスクに組み込んでから、環境依存のpushエラーがほぼゼロになりました。

pushが成功したか必ず検証する

REST APIのもう一つの注意点は、各ステップが200を返しても、最終的に期待どおりブランチが進んでいるとは限らない ことです。途中のSHAを変数で取り違える、ref更新のpayloadを間違える——こうしたミスは例外を投げず、静かに通り過ぎていきます。

私はref更新の直後に、もう一度HEADを取得して、自分が作ったコミットSHAと一致するかを確認するようにしています。

# PATCH 後に検証する
ACTUAL=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \
  "https://api.github.com/repos/${OWNER}/${REPO}/git/refs/heads/${BRANCH}" \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['object']['sha'])")
 
if [ "${ACTUAL}" = "${COMMIT_SHA}" ]; then
  echo "✅ 検証OK: ブランチは ${COMMIT_SHA} に進みました"
else
  echo "❌ 不一致: 期待 ${COMMIT_SHA} / 実際 ${ACTUAL}"
fi

「pushしたつもり」で終わらせないこの一手間が、無人のスケジュールタスクに任せたときの安心感につながります。

全体を振り返って

git CLIに依存しないデプロイフローを用意しておくと、環境の差異に振り回されなくなります。特にエージェント型のツールやCI/CDパイプラインでは、「ローカルのgit状態が信頼できない」前提で設計するのが現実的です。

GitHub REST APIは公式のドキュメントが充実していて、RateLimit(認証済みで5,000 req/h)も通常の用途では十分です。まずは小さなスクリプトで試してみると、意外とシンプルに動くことを実感できると思います。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

Claude Code2026-05-27
Claude Code Bash ツールで /tmp が Permission denied になる時の対処 — $HOME/repos フォールバックパターン
Claude Code のサンドボックス Bash で git clone が `Permission denied` で止まる症状の原因と、$HOME/repos へ自動退避させる実装パターンを、4サイト並行運用での実体験とともに整理します。
Claude Code2026-07-25
strictAllowlist で Claude Code サンドボックスの外向き通信を締める
Claude Code v2.1.219 の sandbox.network.strictAllowlist を使い、自動化パイプラインの外向き通信を最小の許可リストで締める実践メモ。接続先を洗い出すスクリプトと運用の落とし穴まで。
Claude Code2026-07-14
push先がいつの間にか一つ増えていた — /commit-push-pr が origin 以外のリモートへも押し始めた変更に、許可リストで蓋をする
7月14日の更新で /commit-push-pr が origin に加えて設定済みの push リモートへも自動で押すようになりました。便利な一方、ミラーやバックアップ用の第二リモートを持つ環境では、意図しない push が静かに増えます。どのリモートへ押し得るかを棚卸しし、許可リスト外の push を pre-push フックで止め、無人実行でも安全に回す設計を実装込みで整理しました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →