claude-code-actionでsymlinkのCLAUDE.mdがcpSyncで落ちる問題と対処法
CLAUDE.mdがsymlinkだとclaude-code-actionがENOENTで落ちる不具合の原因と、v1.0.123とv1.0.166の2段階の修正、確認方法をまとめました。
anthropics/claude-code-action@v1をGitHub Actionsで使うリポジトリで、CLAUDE.mdをシンボリックリンクにしているとENOENT: no such file or directory, symlinkでジョブがクラッシュすることがあります。action内部のcpSync呼び出しがシンボリックリンクを想定していなかったのが原因です。報告ではv1.0.89で入った退行で、v1.0.123で一次修正、v1.0.166で別の再現条件への修正が入っています。
何が起きるか — エラーの実体
claude-code-actionはPRのheadをチェックアウトした直後、.claudeやCLAUDE.mdなどの機密パスをrestoreConfigFromBaseで扱います。PR側の内容を.claude-pr/へ退避してから、ベースブランチの内容へ差し戻す処理です。PRの投稿者がこれらのファイルを書き換えて、悪意あるフックや設定を仕込めないようにするための仕組みです。
機密パスのどれかがシンボリックリンクだと、退避の段階で次のログとともにジョブが失敗します。
Restoring .claude, .mcp.json, .claude.json, .gitmodules, .ripgreprc, CLAUDE.md, CLAUDE.local.md, .husky from origin/main (PR head is untrusted)
##[error]Action failed with error: ENOENT: no such file or directory, symlinkこの不具合はclaude-code-action#1187として2026年4月6日に報告されました。報告者によれば、v1.0.89以降で発生します。v1.0.89は4月5日公開で、同日にマージされた「Bunランタイムの設定固定とログ整理」のPR #1174が混入元とされています。このPRでは、復元対象にCLAUDE.md・CLAUDE.local.md・.huskyも加わりました。
再現条件は単純です。CLAUDE.md -> AGENTS.mdのようなシンボリックリンクをgit管理下に置いたリポジトリでPRを開き、actionを起動するだけです。AGENTS.mdを実体にしてCLAUDE.mdをリンクにし、複数のツールで指示ファイルを共有する運用が典型的に踏みます。
暫定の回避策は2つ報告されました。1つはactionのバージョンをv1.0.88にピン留めする方法です。
- uses: anthropics/claude-code-action@v1.0.88もう1つは、issueのコメントでnatsuki-engr氏が書いた方法で、CLAUDE.mdのリンクを実ファイルに置き換えるものです。バージョンを固定できない場合の代替ですが、AGENTS.mdとの内容同期を手作業で保つことになります。
自分のリポジトリが影響を受けるか確認する
機密パスの8種類のうち、どれかがシンボリックリンクになっていないかは、ローカルでfindを使えば調べられます。.claudeの中に置いたリンクも拾うため、ls -laより漏れません。
find .claude .mcp.json .claude.json .gitmodules .ripgreprc \
CLAUDE.md CLAUDE.local.md .husky -type l -ls 2>/dev/null一時ディレクトリにCLAUDE.md -> AGENTS.mdと.claude/CLAUDE.md -> ../AGENTS.mdを作って実行したときの出力(リンク名とリンク先の列のみ)は次のとおりです。
CLAUDE.md -> AGENTS.md
.claude/CLAUDE.md -> ../AGENTS.md1行でも出れば、そのパスがこの不具合の対象です。何も出なければ、機密パスはすべて実ファイルか存在しないかのどちらかです。
claude-code-actionとclaude-codeCLIは同じanthropics配下のプロジェクトですが、別物です。CLIにはワークツリー間でnode_modulesを共有するworktree.symlinkDirectoriesというシンボリックリンク機能があります。ただ、今回の不具合はGitHub Actions上のactionが機密パスを退避する処理で起きるもので、CLIのローカル実行やworktreeとは別の経路です。
原因はBunのcpSyncが親ディレクトリを作り損ねること
退避処理は、機密パスを.claude-pr/へcpSync(p, dest, { recursive: true })でコピーしていました。dereferenceの既定値はfalseなので、シンボリックリンクはリンクのまま複製されます。コピー先の.claude-pr/はまだ存在しません。Bunの実装ではリンクの複製がこの親ディレクトリを作らず、ENOENTで落ちます。
issue本文は、親ディレクトリが無いままsymlink()が呼ばれることを原因に挙げています。ところが同じ呼び出しは、Node.jsでは通ります。Node v20.18.0で、リンクをコピー先の存在しないディレクトリへ複製してみた結果です。
node -e '
const { cpSync, symlinkSync, writeFileSync } = require("fs");
writeFileSync("target.md", "# Hello");
symlinkSync("target.md", "link.md");
cpSync("link.md", "out2/link.md", { recursive: true });
cpSync("link.md", "out3/link.md", { recursive: true, dereference: true });
'
ls -l out2 out3out2:
lrwxr-xr-x 1 user wheel 32 Oct 4 22:28 link.md -> /tmp/xxxx/target.md
out3:
-rw-r--r-- 1 user wheel 7 Oct 4 22:28 link.mdどちらもエラーなしで終わります。out2とout3は事前に作っていませんが、Nodeが親ディレクトリを作っています。dereferenceを付けない場合は、リンク先が絶対パスに解決されたリンクができ、付けた場合は中身が通常ファイルとして入ります。
claude-code-actionがBunランタイム上で動くことが分かれ目でした。報告者の1人davidpoblador氏が、根本原因はBun側だと指摘し、oven-sh/bun#28997として報告しています。Bunはネイティブ(Zig言語)実装の高速パスを持ちます。通常ファイルの複製では、親ディレクトリが無いとmkdirして再試行しますが、シンボリックリンクの複製だけはその再試行を通りません。
Bun側のissueには、実装の箇所も書かれています。macOSではcopyfile()を、Linuxではsymlink()を直接呼び、どちらもエラーのまま戻る作りです。GitHub ActionsのLinuxランナーでsymlinkの文言が出るのは、このためです。macOSでは同じ不具合がcopyfileのENOENTとして現れます。
dereference: trueを指定すると、BunはNode.js由来のJSフォールバックを使います。こちらはコピー元の種類を問わず先にcheckParentDir()を呼ぶため、親ディレクトリの欠落で落ちません。PR #1186はこの性質を使った、actionのコード側の迂回策です。
修正の経緯 — Bunの修正、action側の迂回、そして2つ目のエラー
symlink ENOENT の経過
- 2026-04-05v1.0.89 で退行が入る
Bunランタイムの設定固定を含むPR #1174がマージされました。v1.0.89から、機密パスのリンクで落ちます。
- 2026-04-06issue #1187 が報告される
CLAUDE.md -> AGENTS.mdで再現し、v1.0.88へのピン留めが回避策として挙がりました。 - 2026-05-12Bun v1.3.14 がリリースされる
Bun側のissueのコメントによると、LinuxとFreeBSDでリンクの複製時の
ENOENTが直りました。issueは5月13日に閉じられています。 - 2026-05-14v1.0.123 に PR #1186 と Bun 更新が入る
dereference: trueの追加(PR #1186)と、固定するBunを1.3.14へ上げる変更(PR #1312)が同じリリースに入りました。 - 2026-06-09issue #1398 が報告される
Bun側の修正後のactionでも、
.claude/CLAUDE.md -> ../AGENTS.mdのリンクで別のENOENTが出ました。 - 2026-07-06v1.0.166 がリリースされる
7月4日にマージされたPR #1441が入り、リンク先が見つからないときのフォールバックが追加されました。
issue #1187の時点でdavidpoblador氏は、actionが固定するBunは1.3.6のままで、dereference: trueが入るかBunが上がるまで踏み続けると書いていました。両方が入ったv1.0.123以降は、actionとBunのどちらの側でも直っています。
issue #1398のログは、2つ目のエラーを示しています。Claude Code 2.1.169をインストールした実行で、Restoring ...の直後に出たのはsymlinkではなくstatxでした。
##[error]Action failed with error: ENOENT: no such file or directory,
statx '.claude/CLAUDE.md'dereference: trueは「リンク先の中身をコピーする」動作です。PR #1441の本文によると、リンク先がPRのheadに無いと、リンクをたどる段階でstatxがENOENTを返します。コード内コメントは、.claude/CLAUDE.md -> ../AGENTS.mdでPRがAGENTS.mdを削除した場合を例に挙げています。修正は、中身のコピーがENOENTで失敗したときだけ、リンクのまま複製し直す形でした。
function snapshotSensitivePath(src: string, dest: string): void {
try {
cpSync(src, dest, { recursive: true, dereference: true });
} catch (error) {
// Symlinks whose targets are absent on the PR head (e.g. `.claude/CLAUDE.md`
// -> `../AGENTS.md` when the PR deleted the target) make dereferenced
// copies throw ENOENT. Preserve the symlink for the review snapshot instead.
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
cpSync(src, dest, { recursive: true });
return;
}
throw error;
}
}その後、v1.0.187に入ったPR #1596で、restore-config.tsの退避はさらに書き換わりました。スナップショットにリンクを一切含めない設計です。中身をコピーするのは、次の4条件をすべて満たすエントリだけです。
- リンクの実体が作業ツリーの内側にある
- 実体の経路に
.gitが含まれず、.claude-pr/自身の中でもない - 実体が、エントリ自身の親ディレクトリを含む循環になっていない
- リンク経由で届くなら、HEADから変更されていないgit追跡済みの内容である
条件を満たさないエントリは、was a symbolic link to "../AGENTS.md"; the link target is not included in this snapshotのような一文を書いた通常ファイルで置き換えます。リンク先が存在しなくても、退避の途中で参照先が消えても、restoreは止まりません。
機密パスのリストはCLIのProtected pathsとどこが重なるか
restore-config.tsの機密パス(SENSITIVE_PATHS)は、.claude・.mcp.json・.claude.json・.gitmodules・.ripgreprc・CLAUDE.md・CLAUDE.local.md・.huskyの8種類です。コードのコメントによれば、PRが変更でき、かつCLIが起動時にカレントディレクトリから読むパスが対象です。
Claude Code CLIのProtected pathsと見比べると、CLAUDE.mdとCLAUDE.local.mdを除く6種類が重なります。ただし役割は別で、Protected pathsはCLIが手元の実行中に自分自身の書き込みを止める仕組みです。claude-code-actionの処理は、PRのheadが持ち込んだ改変をベースブランチの内容に戻す、Actions側の安全装置です。
どのバージョンが影響を受けるか
症状別の切り分け
symlink で ENOENT
機密パスがリンクならsymlinkのエラーで落ちます。v1.0.88へのピン留めか、v1.0.123以降への更新で避けられます。
statx で ENOENT
PR #1441が直したのは、リンク先がPRのheadに無いケースです。v1.0.166以降は、リンクのまま複製して続行します。
v1.0.88のような特定バージョンやコミットSHAで固定しているワークフローは、固定先がv1.0.123未満かどうかを確かめます。機密パス(8種類)にシンボリックリンクを持たないリポジトリは、この不具合の対象外です。
まとめ
運用を変えずに直したいなら、v1.0.187以降が候補になります。リンク先がPRで消えても、退避は止まらず続行します。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- github.com/anthropics/claude-code-action/issues/1187
- github.com/anthropics/claude-code-action/pull/1186
- github.com/anthropics/claude-code-action/pull/1441
- github.com/anthropics/claude-code-action/blob/main/src/github/operati…
- github.com/oven-sh/bun/issues/28997
- github.com/anthropics/claude-code-action/issues/1398
- github.com/anthropics/claude-code-action/pull/1312
- github.com/anthropics/claude-code-action/pull/1596
- github.com/anthropics/claude-code-action/blob/main/action.yml