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は並行して作成できるので、ファイル数が増えても待ち時間は最小限に抑えられます。
エラーハンドリングのポイント
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エラーがほぼゼロになりました。
全体を振り返って
git CLIに依存しないデプロイフローを用意しておくと、環境の差異に振り回されなくなります。特にエージェント型のツールやCI/CDパイプラインでは、「ローカルのgit状態が信頼できない」前提で設計するのが現実的です。
GitHub REST APIは公式のドキュメントが充実していて、RateLimit(認証済みで5,000 req/h)も通常の用途では十分です。まずは小さなスクリプトで試してみると、意外とシンプルに動くことを実感できると思います。
自分の現場で当てはめる3つの質問
- この機能が止まったとき、ユーザー体験はどの程度劣化するか
- 失敗を検知する仕組みは備わっているか
- 戻す手順は手元のドキュメントに書いてあるか