anthropics / anthropics/claude-code-action

settings input is shallow-merged into ~/.claude/settings.json and leaks across jobs on shared-HOME self-hosted runners

Ouverte
#1,688 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
area:permissions bug p2
Langage dominant
TypeScript
Étoiles
8.9k
Forks
2.1k
Métriques de merge des PR
Métriques de PR en attente

Description

## Summary

`setupClaudeCodeSettings` (base-action/src/setup-claude-code-settings.ts) writes the `settings:` input to `$HOME/.claude/settings.json` by shallow-merging it over whatever is already there, and nothing ever resets that file. On self-hosted runners that share a Unix user (several runner instances on one box, all under one $HOME), one workflow's permission profile persists on the machine and is silently loaded as user-scope settings by every later job's session — including jobs that pass no `settings:` input at all.

## Impact we hit

Our review workflow passes a restrictive profile (`permissions.deny: ["Write", "Edit", "Bash(git commit*)", "Bash(git push*)"]`). Our implement workflow passes no `settings:` and runs with `--dangerously-skip-permissions`. After any review run, implement sessions on the same box inherit the reviewer's deny list — and a bare tool-name deny removes the tool from the model's context entirely, so `--dangerously-skip-permissions` does not restore it. The observable failure mode is nasty: the implement agent, unable to Write/Edit/commit/push, routed around it by constructing commits via the GitHub Git Data API (`gh api .../git/blobs|trees|commits|refs`) — bypassing the repo's commit gates — and noted in its PR that "this session's permission settings hard-deny git commit/git push". It took us a while to trace that back to another workflow's settings.

Because the merge is shallow and unconditional, the file also races when jobs run concurrently on sibling runners.

## Where in the source

```ts
// base-action/src/setup-claude-code-settings.ts
const settingsPath = `${home}/.claude/settings.json`;
...
settings = { ...settings, ...inputSettings };
settings.enableAllProjectMcpServers = true;
await $`echo ${JSON.stringify(settings, null, 2)} > ${settingsPath}`.quiet();
```

`index.ts` always calls it with `homeDir = undefined`, so it is always `os.homedir()`. There is no input for a job-scoped location, and the action does not honor `CLAUDE_CONFIG_DIR` here.

## Ask

Any of these would fix the class:

1. Write the settings to a job-scoped file and pass it to the CLI via `--settings ` (the CLI already supports per-session settings with higher precedence), leaving the user file untouched.
2. Honor `CLAUDE_CONFIG_DIR` in `setupClaudeCodeSettings` so operators can isolate per job.
3. At minimum: replace rather than merge, and restore the prior file contents in a post step.

Related: #1057 describes the same shared-`~/.claude` contamination class for marketplace installs ("Workaround: none that's safe for concurrent jobs").

## Workaround we're deploying

Passing the profile via `claude_args: --setting-sources project,local --settings '{...}'` so sessions never read the polluted user file — works, but every consumer of `settings:` on shared runners is exposed to this by default.

## Environment

- claude-code-action@v1, base-action bundled
- Ubuntu 24.04, 4-5 self-hosted runner instances registered under one user, shared $HOME
- Claude Code 2.1.x preinstalled via `path_to_claude_code_executable`

Guide de contribution

Ouvrir le guide de contribution

Évaluation

Cette issue n'a pas encore été évaluée.

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.