Skip to content

Stale prompt files in ${RUNNER_TEMP}/claude-prompts/ leak between jobs on non-ephemeral self-hosted runners #1287

@kyungilpark

Description

@kyungilpark

Summary

On non-ephemeral self-hosted runners, claude-code-action writes prompt files to ${RUNNER_TEMP}/claude-prompts/, a directory that — contrary to the GitHub Actions documented contract — is not reliably emptied between jobs. Files written by one invocation persist on disk and are picked up by subsequent invocations on the same runner agent, causing the model to see stale context from an unrelated prior job.

In our case, a claude-user-request.txt left behind by a @claude mention-mode invocation in repo A was repeatedly read by later prompt-driven review invocations in repo B running on the same runner agent. The model dutifully addressed the stale question in PR review comments — visible cross-job context bleed.

Environment

  • Action: anthropics/claude-code-action@v1.0.67
  • Runner: self-hosted, non-ephemeral, Linux (one runner agent processes many jobs sequentially)
  • GHES (but the bug is environment-agnostic — same on github.com self-hosted)

Reproduction

  1. Configure two workflows on the same self-hosted runner pool:

    • Workflow M: claude-mention.yml-style — invokes the action with prompt: "" and relies on @claude mention triggering
    • Workflow R: claude-code-review.yml-style — invokes the action with a non-empty prompt: (agent mode, e.g. "You are a code reviewer. Review this PR.")
  2. Trigger Workflow M with a mention comment containing some distinctive text, e.g. @claude what's the difference between approach X and approach Y?. Note the runner agent name that picks it up (Set up job log shows Runner name: …).

  3. Wait for the job to finish. Inspect the runner host:

    $ ls -la $RUNNER_TEMP/claude-prompts/
    -rw-r--r-- claude-prompt.txt        (the assembled prompt — overwritten each run)
    -rw-r--r-- claude-user-request.txt  (the user request from step 2 — not cleared)
    
  4. Now trigger Workflow R against the same runner agent by submitting a PR. (Re-run if needed until Runner name matches.)

  5. Observe Workflow R's review output. With non-trivial probability the review body or comment will reference the stale text from step 2 ("regarding your earlier question about X vs Y…", or "this question doesn't seem relevant to this PR — please clarify…"), demonstrating that step-2 content reached the step-4 model context.

We observed this happen on 5 of 12 review jobs that landed on the affected runner agent over a 5-day window — roughly 40% manifest rate. The other 7 ran the review correctly. The same runner agent never self-recovered; only manual rm -rf $RUNNER_TEMP/claude-prompts/ cleared it.

Root cause

src/create-prompt/index.ts (v1.0.67):

await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, { recursive: true });

await writeFile(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`, promptContent);

await writeFile(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/${USER_REQUEST_FILENAME}`, userRequest);

Two compounding issues:

  1. Path assumption: GitHub's documentation states RUNNER_TEMP "is emptied at the beginning and end of each job." On hosted runners (ephemeral) this is trivially true. On self-hosted runners it is widely known to not be honored — the runner agent does not actively wipe _work/_temp/ between jobs. Action authors targeting self-hosted runners cannot rely on this contract.

  2. Mode-dependent overwrite: claude-prompt.txt is rewritten every invocation, so it self-heals. But claude-user-request.txt is only written when there is a user request (mention mode). In agent-mode (non-empty prompt input) the file is left untouched. So a single mention-mode invocation can leave a claude-user-request.txt that persists indefinitely and is incorporated into every subsequent agent-mode invocation's effective context on the same runner.

Expected behavior

Each invocation should see a clean claude-prompts/ directory containing only files written by that specific invocation, regardless of runner ephemerality.

Suggested fixes (any one would resolve)

  1. Per-invocation subdirectory. Write to ${RUNNER_TEMP}/claude-prompts-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}/ (or use mkdtemp). Cleanly isolates jobs; no shared mutable directory.
  2. Cleanup at the start. await rm(${RUNNER_TEMP}/claude-prompts, { recursive: true, force: true }) before the mkdir. One line, full self-healing, idempotent.
  3. Always-write claude-user-request.txt. Even in agent mode, write an empty (or sentinel) file so any prior content is unconditionally clobbered. Doesn't fix the general directory-staleness issue but kills this specific leak.

Option 2 is the smallest, least invasive change.

Workaround (if anyone hits this before the fix lands)

In a wrapping composite or as a step before the action call:

- name: Clear stale claude-prompts artifacts
  shell: bash
  run: |
    target="${RUNNER_TEMP:-/tmp}/claude-prompts"
    if [ -d "$target" ]; then
      rm -rf "$target"
    fi

Sufficient guard; tested and effective.

Related issues

The pattern across these is consistent: the action repeatedly assumes per-job state isolation that self-hosted runners do not actually provide. A repo-wide audit of every cache/state directory under RUNNER_TEMP (and similar) would likely surface more.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp2Non-showstopper bug or popular feature request

    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