Summary
restoreConfigFromBase in src/github/operations/restore-config.ts crashes the action with ENOENT: no such file or directory, symlink on any PR whose head has CLAUDE.md but no .claude/ directory.
Repro
- Open a PR whose head contains
CLAUDE.md at the repo root and does not contain any of the other SENSITIVE_PATHS entries (.claude, .mcp.json, .claude.json, .gitmodules, .ripgreprc, CLAUDE.local.md, .husky).
- Trigger the action via
@claude (or any tag-mode entry).
Failing run from our repo: https://github.com/EnderTuringHQ/SpeechEngine/actions/runs/25823997412 (job Run Claude Code).
Failure log
Restoring .claude, .mcp.json, .claude.json, .gitmodules, .ripgreprc, CLAUDE.md, CLAUDE.local.md, .husky from origin/dev (PR head is untrusted)
##[error]Action failed with error: ENOENT: no such file or directory, symlink
…
Internal error: directory mismatch for directory "/home/runner/work/_actions/anthropics/claude-code-action/<sha>/tsconfig.json", fd 4. You don't need to do anything, but this indicates a bug.
##[error]Process completed with exit code 1.
Root cause
In src/github/operations/restore-config.ts:
rmSync(".claude-pr", { recursive: true, force: true });
for (const p of SENSITIVE_PATHS) {
if (existsSync(p)) {
cpSync(p, `.claude-pr/${p}`, { recursive: true });
}
}
After rmSync(".claude-pr", …), the .claude-pr/ directory does not exist. When the loop hits CLAUDE.md (a single regular file at repo root), it calls cpSync("CLAUDE.md", ".claude-pr/CLAUDE.md", { recursive: true }). Under Bun's cpSync (the action runs under Bun), copying a single file into a non-existent parent directory throws ENOENT from an internal symlink-based copy fallback — even with recursive: true, which only auto-creates intermediate directories when the source is a directory.
The reason it has worked historically: most PRs include a .claude/ directory, which is processed first alphabetically; copying a directory with recursive: true does auto-create the destination parent, so subsequent single-file copies (like CLAUDE.md) find .claude-pr/ already in place. PRs that omit .claude/ but keep CLAUDE.md skip that step and hit the bug directly.
Suggested fix
One line before the loop:
mkdirSync(".claude-pr", { recursive: true });
(or wrap each cpSync in mkdirSync(dirname(dest), { recursive: true }) for defense-in-depth).
Regression range
We've pinned back to v1.0.88 as a workaround. Happy to test a PR if you want a second pair of eyes.
Summary
restoreConfigFromBaseinsrc/github/operations/restore-config.tscrashes the action withENOENT: no such file or directory, symlinkon any PR whose head hasCLAUDE.mdbut no.claude/directory.Repro
CLAUDE.mdat the repo root and does not contain any of the otherSENSITIVE_PATHSentries (.claude,.mcp.json,.claude.json,.gitmodules,.ripgreprc,CLAUDE.local.md,.husky).@claude(or any tag-mode entry).Failing run from our repo: https://github.com/EnderTuringHQ/SpeechEngine/actions/runs/25823997412 (job
Run Claude Code).Failure log
Root cause
In
src/github/operations/restore-config.ts:After
rmSync(".claude-pr", …), the.claude-pr/directory does not exist. When the loop hitsCLAUDE.md(a single regular file at repo root), it callscpSync("CLAUDE.md", ".claude-pr/CLAUDE.md", { recursive: true }). Under Bun'scpSync(the action runs under Bun), copying a single file into a non-existent parent directory throwsENOENTfrom an internal symlink-based copy fallback — even withrecursive: true, which only auto-creates intermediate directories when the source is a directory.The reason it has worked historically: most PRs include a
.claude/directory, which is processed first alphabetically; copying a directory withrecursive: truedoes auto-create the destination parent, so subsequent single-file copies (likeCLAUDE.md) find.claude-pr/already in place. PRs that omit.claude/but keepCLAUDE.mdskip that step and hit the bug directly.Suggested fix
One line before the loop:
(or wrap each
cpSyncinmkdirSync(dirname(dest), { recursive: true })for defense-in-depth).Regression range
restore-config.tshas no.claude-pr/snapshot loop.restore-config.tsis byte-identical between v1.0.89 and v1.0.121.We've pinned back to v1.0.88 as a workaround. Happy to test a PR if you want a second pair of eyes.