anthropics / anthropics/claude-code-action
settings input is shallow-merged into ~/.claude/settings.json and leaks across jobs on shared-HOME self-hosted runners
- 主要言語
- TypeScript
- スター
- 8.9k
- フォーク
- 2.1k
- PR マージ指標
- PR 指標を取得中
説明
## 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`
コントリビューションガイド
評価
この issue はまだ評価されていません。