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
-
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.")
-
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: …).
-
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)
-
Now trigger Workflow R against the same runner agent by submitting a PR. (Re-run if needed until Runner name matches.)
-
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:
-
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.
-
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)
- 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.
- Cleanup at the start.
await rm(${RUNNER_TEMP}/claude-prompts, { recursive: true, force: true }) before the mkdir. One line, full self-healing, idempotent.
- 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.
Summary
On non-ephemeral self-hosted runners,
claude-code-actionwrites 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.txtleft behind by a@claudemention-mode invocation in repo A was repeatedly read by laterprompt-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
anthropics/claude-code-action@v1.0.67Reproduction
Configure two workflows on the same self-hosted runner pool:
claude-mention.yml-style — invokes the action withprompt: ""and relies on@claudemention triggeringclaude-code-review.yml-style — invokes the action with a non-emptyprompt:(agent mode, e.g."You are a code reviewer. Review this PR.")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 joblog showsRunner name: …).Wait for the job to finish. Inspect the runner host:
Now trigger Workflow R against the same runner agent by submitting a PR. (Re-run if needed until
Runner namematches.)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):Two compounding issues:
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.Mode-dependent overwrite:
claude-prompt.txtis rewritten every invocation, so it self-heals. Butclaude-user-request.txtis only written when there is a user request (mention mode). In agent-mode (non-emptypromptinput) the file is left untouched. So a single mention-mode invocation can leave aclaude-user-request.txtthat 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)
${RUNNER_TEMP}/claude-prompts-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_JOB}/(or usemkdtemp). Cleanly isolates jobs; no shared mutable directory.await rm(${RUNNER_TEMP}/claude-prompts, { recursive: true, force: true })before themkdir. One line, full self-healing, idempotent.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:
Sufficient guard; tested and effective.
Related issues
'already installed'on non-ephemeral self-hosted runners) — sibling: another piece of action state that assumes ephemeral cleanupThe 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.