From bb5dee268e138d39f5c80ea002320ced15b8e39c Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Sat, 17 Oct 2026 20:12:00 +0200 Subject: [PATCH] fix(ci): refuse a committed .claude/settings.local.json in sandbox-lint Since #1684 the committed .claude/settings.json is the shared floor and .claude/settings.local.json is each contributor's own layer, reconciled from the local template. The local file is gitignored but can be force-added, and a committed copy would be loaded for everyone who checks the repository out: its scalars override the floor's (`sandbox.enabled: false`), and CI lints only the floor and the template. #1684 dropped the step that refused committed agent settings. The workflow now runs when .claude/settings.local.json changes and fails while it is tracked, pointing the author at the floor or the template. Generated-by: Claude Opus 5 --- .asf.yaml | 2 +- .github/workflows/sandbox-lint.yml | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.asf.yaml b/.asf.yaml index f16020e98..3ed62c91a 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -125,7 +125,7 @@ github: # would block the PR). Excluded for that reason: # `asf-allowlist-check` (paths: `.github/**`) and # `lint .claude/settings.json against baseline` from - # sandbox-lint (paths: `.claude/settings.json`, + # sandbox-lint (paths: `.claude/settings.json`, `.claude/settings.local.json`, # `tools/agent-isolation/claude-settings.local.template.json`, # `plugins/magpie-setup/skills/setup/family-mcp-allows.json`, # `.gemini/**` and `tools/sandbox-lint/**`; see `paths:` in diff --git a/.github/workflows/sandbox-lint.yml b/.github/workflows/sandbox-lint.yml index f8509da35..5506e3be4 100644 --- a/.github/workflows/sandbox-lint.yml +++ b/.github/workflows/sandbox-lint.yml @@ -28,6 +28,9 @@ on: # yamllint disable-line rule:truthy pull_request: paths: - ".claude/settings.json" + # Each contributor's own layer, never tracked: a change here means + # someone committed one, and the job must run to refuse it. + - ".claude/settings.local.json" - "tools/agent-isolation/claude-settings.local.template.json" - "plugins/magpie-setup/skills/setup/family-mcp-allows.json" - ".gemini/**" @@ -37,6 +40,9 @@ on: # yamllint disable-line rule:truthy branches: [main] paths: - ".claude/settings.json" + # Each contributor's own layer, never tracked: a change here means + # someone committed one, and the job must run to refuse it. + - ".claude/settings.local.json" - "tools/agent-isolation/claude-settings.local.template.json" - "plugins/magpie-setup/skills/setup/family-mcp-allows.json" - ".gemini/**" @@ -85,6 +91,17 @@ jobs: # repo root, which is what the linter expects so # `.claude/settings.json` and `tools/sandbox-lint/expected.json` # resolve relative to cwd. + # `.claude/settings.local.json` is each contributor's own layer over the + # committed floor, reconciled from the local template. Claude Code + # loads it for everyone who checks the repository out, and a local + # scalar overrides the floor's (`sandbox.enabled: false`), but CI lints + # only the floor and the template. Refuse a committed copy outright. + - name: Refuse a committed .claude/settings.local.json + run: | + if git ls-files --error-unmatch .claude/settings.local.json >/dev/null 2>&1; then + echo "::error file=.claude/settings.local.json::.claude/settings.local.json is each contributor's own layer and must not be committed. Shared settings go in the floor (.claude/settings.json) or the local template (tools/agent-isolation/claude-settings.local.template.json); remove the file with 'git rm --cached .claude/settings.local.json'." + exit 1 + fi - name: Run sandbox-lint run: uv run sandbox-lint - name: Check Gemini sandbox and policy profile