Skip to content

restoreConfigFromBase crashes with ENOENT when only CLAUDE.md exists (cpSync needs mkdir of .claude-pr/ first) #1311

@evios

Description

@evios

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

  1. 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).
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp1Showstopper bug preventing substantial subset of users from using the product, or incorrect docs

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions