claude-code-actionでsymlinkのCLAUDE.mdがcpSyncで落ちる問題と対処法
CLAUDE.mdがsymlinkだとclaude-code-actionがENOENTでクラッシュする不具合の原因と、2段階にわたる修正の経緯をまとめました。
anthropics/claude-code-action@v1をGitHub Actionsで使うリポジトリで、CLAUDE.mdをシンボリックリンクにしているとENOENT: no such file or directory, symlinkでジョブがクラッシュすることがあります。原因はaction内部のcpSync呼び出しがシンボリックリンクを想定していなかったことで、2026年5月に一次修正が入ったものの、別の再現条件が7月まで残っていました。
何が起きるか — エラーの実体
claude-code-action@v1はPRのheadをチェックアウトした直後、.claudeやCLAUDE.mdなどの機密パスをベースブランチの内容へ差し戻すrestoreConfigFromBaseを実行します。PRの投稿者がこれらのファイルを書き換えて悪意あるフック・設定を仕込めないようにするための処理です。このときCLAUDE.md(または.claudeディレクトリの中身)がシンボリックリンクになっていると、次のログとともにジョブが失敗します。
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日に報告されました。再現条件は単純で、CLAUDE.md -> AGENTS.mdのようなシンボリックリンクをgit管理下に置いたリポジトリでPRを開き、claude-code-actionを起動するだけです。AGENTS.mdとCLAUDE.mdを両方用意し、後者を前者へのリンクにして共有する運用をしているリポジトリが典型的に踏みます。クラッシュの対象は.claudeディレクトリ配下や.mcp.json、.huskyなど複数ありますが、issueに寄せられた報告はCLAUDE.mdのケースが大半でした。
暫定の回避策は2つ報告されています。1つはactionのバージョンをピン留めする方法です。
- uses: anthropics/claude-code-action@v1.0.88もう1つは、issueのコメントでnatsuki-engr氏が報告した方法で、CLAUDE.mdのシンボリックリンクを実ファイルへ置き換えることです。actionのバージョンを固定できない事情がある場合の代替になります。ただしAGENTS.mdとの内容同期を手動で保つ必要が生じるため、根本的にはaction側の修正版へ上げる方が運用コストは低くなります。
自分のリポジトリが影響を受けるか確認する
SENSITIVE_PATHSに含まれるファイル・ディレクトリのうち、どれかがシンボリックリンクになっていないかはローカルで確認できます。
ls -la CLAUDE.md CLAUDE.local.md .claude .mcp.json .claude.json .husky 2>/dev/null | grep '\->'出力に矢印(->)付きの行が出れば、その対象がこの不具合の影響を受けます。claude-code-actionとclaude-codeCLIは同じanthropics配下のプロジェクトですが別物です。CLI側にはワークツリー間でnode_modulesを共有するためのworktree.symlinkDirectoriesというシンボリックリンク機能がありますが、今回の不具合はGitHub Actions上のaction固有の処理で起きるもので、CLIのローカル実行やworktree機能とは別の経路です。
原因はcpSyncの既定挙動とBunのファストパス実装
restore-config.tsが差し戻し対象とする機密パス(SENSITIVE_PATHS)は.claude・.mcp.json・.claude.json・.gitmodules・.ripgreprc・CLAUDE.md・CLAUDE.local.md・.huskyの8種類です。このリストはClaude Code CLI自身が持つProtected pathsのリストと重なる部分が多く、.claude・.mcp.json・.claude.json・.huskyは両方に含まれます。どちらも「書き換わるとClaude Codeの挙動や権限設定自体が変わってしまうファイル」を対象にしている点は共通です。ただしProtected pathsはCLIがローカル実行中に自分自身の書き込みを止める機構であるのに対し、claude-code-actionのこの処理はGitHub Actions上でPRのheadブランチが持ち込む改変をベースブランチの内容へ差し戻す、別レイヤーの安全装置です。
差し戻し処理はrestore-config.tsが機密パスを.claude-pr/へコピーする際にcpSync(p, dest, { recursive: true })を呼んでいました。Node.jsのcpSyncはdereferenceオプションの既定値がfalseで、これはシンボリックリンクをリンクのまま複製する(中身を追わない)動作です。通常のファイルなら親ディレクトリが無くてもmkdirしてから書き込みますが、シンボリックリンクの複製経路ではsymlink()システムコールをそのまま呼ぶため、宛先の親ディレクトリがまだ存在しないとENOENTで失敗します。
claude-code-actionはBunランタイム上で動くactionです。報告者の1人davidpoblador氏の調査によると、これはNode.jsのcpSyncの仕様差ではなく、BunのcpSync実装に限定されたバグでした。Bunはネイティブ(Zig言語)実装の高速パスを持ち、通常ファイルのコピーでは親ディレクトリの欠落を検知して自動的にmkdirしてからリトライする処理が入っています。ところがシンボリックリンクの複製コードパスだけはこのリトライ処理を素通りしていました。dereference: trueを指定するとBunはネイティブ高速パスではなくNode.js移植のJSフォールバック実装を使うようになります。そちらは全てのソース種別でcheckParentDir()を呼ぶため、この不具合を回避できます。この根本原因は上流のoven-sh/bun#28997として報告されています。
dereference: trueはaction側のコードでBunのバグを迂回する対症療法でした。Bun本体のバグそのものは、Bun v1.3.14で修正されたとみられます。davidpoblador氏は2026年5月13日の時点で「Bun本体の修正は入ったとみられるが、action.ymlはまだ1.3.6を固定したままなので、dereference: trueが入るかBunのバージョンが上がるまでは踏み続ける」とコメントしています。claude-code-actionのaction.ymlは現在このv1.3.14をbun-versionとして固定しており、action起動時にインストールされるBunランタイム自体もこの不具合を踏まない状態になっています。
修正は1回では終わらなかった
この不具合の修正はコード1行の変更では収まらず、2段階に分けて入りました。
| 時期 | 変更 | 内容 |
|---|---|---|
| 2026-04-06 | 変更claude-code-action#1187報告 | 内容CLAUDE.mdがsymlinkだとENOENTでクラッシュ |
| 2026-05-14マージ | 変更PR #1186 | 内容cpSync呼び出しにdereference: trueを追加。symlinkを中身ごとコピーするよう変更 |
| 2026-07-04マージ | 変更PR #1441 | 内容PR #1186のフィックス自体が別のENOENTを起こすケースへの対処 |
PR #1186はdereference: true一つで大半のケースを直しましたが、これは「リンク先の実体をコピーする」動作への変更でもあります。リンク先がPRのheadブランチ上に存在しない場合(例えば.claude/CLAUDE.md -> ../AGENTS.mdのリンクに対し、PRがAGENTS.md自体を削除していた場合)、dereferenceしたコピーは今度は別のENOENTを投げるようになりました。PR #1441はこのケースをtry/catchで捕捉し、dereferenceしたコピーが失敗したときだけシンボリックリンクをリンクのまま複製するフォールバックを追加しています。
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 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;
}
}その後mainブランチではこの処理はさらに書き換わっていて、シンボリックリンクの参照先が作業ツリー内の追跡済み(git管理下で未変更)コンテンツを指す場合だけ中身をコピーし、そうでないリンクはコピーせずプレースホルダーファイルとして記録する実装になっています。判定条件は主に4つで、①参照先が作業ツリーの外を指していないか、②参照先の経路に.gitやスナップショット先自身が含まれていないか、③参照先が自分自身の親ディレクトリを含む循環になっていないか、④リンク経由でたどり着いたファイルはHEADから変更されていないgit追跡済みの内容か、をすべて満たすときだけ中身をコピーします。これはレビューエージェントが参照するスナップショットにシンボリックリンクそのものを一切含めない設計へ寄せたさらなるセキュリティ強化です。未追跡ファイルやPRで変更済みのファイルへリンクしている場合は、中身の代わりに「このパスはスナップショットに含まれていません」というプレースホルダーが置かれます。この記事が扱う「symlinkでENOENTクラッシュする」不具合自体は、PR #1186とPR #1441の時点で解消済みです。
影響を受けるのはどんな環境か
| 状況 | 影響 |
|---|---|
CLAUDE.mdや.claude配下をgit管理下のシンボリックリンクにしている | 影響v1.0.88以前へのピン留めが必要だった(PR #1186マージ後のバージョンは対象外) |
| シンボリックリンクの参照先がPRのheadブランチで削除・改名されている | 影響PR #1186単体では別のENOENTを踏む可能性があった(PR #1441で解消) |
CLAUDE.mdを実ファイルとして持ち、シンボリックリンクを使っていない | 影響この不具合の対象外 |
複数のAIコーディングツール向けにAGENTS.mdを実体、CLAUDE.mdをそのリンクとして共有運用している | 影響典型的にこの不具合を踏む構成 |
AGENTS.mdを単一の実体ファイルとして持ち、CLAUDE.mdや.cursorrulesなどをそこへのリンクにする運用は、複数のAIコーディングツールで指示ファイルを重複管理しないための一般的なテクニックです。claude-code-actionでこの構成を使う場合は、actionのバージョンが2026年5月14日以降(PR #1186マージ後)であることを確認しておくと安全です。
まとめ
claude-code-actionでENOENT: no such file or directory, symlinkが出た場合、原因はCLAUDE.mdなどの機密パスがシンボリックリンクで、action内部のcpSync呼び出しがそれを想定していなかったことにあります。根本はBunのcpSyncネイティブ実装がシンボリックリンクの複製時だけ親ディレクトリのリトライ処理を欠いていたためで、dereference: trueを指定するJSフォールバック経路に切り替えることで回避できます。actionの修正はPR #1186(2026年5月14日)とPR #1441(2026年7月4日)の2段階で入っており、この症状自体は最新版で解消済みです。古いバージョンにピン留めしたままのワークフローが残っている場合は、更新を検討する価値があります。