From a293b8dbc2d12ba78fae3749758da2bf042cfec7 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:46:28 +0000 Subject: [PATCH] Refuse tracked Claude project-local settings Add the durable-workflow/.github#355 tracked-path refusal to scripts/check-public-boundary.sh, before the content scans. It scans the complete tracked tree, prints only the path, and never reads the file. .gitignore now covers root and nested .claude/settings.local.json. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 2 ++ scripts/check-public-boundary.sh | 6 ++++ tests/test_public_boundary_scanner.py | 44 +++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/.gitignore b/.gitignore index f0620d69..7056b600 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ site/ .docs-requirements.txt .docs-published-wheel.txt +.claude/settings.local.json +**/.claude/settings.local.json diff --git a/scripts/check-public-boundary.sh b/scripts/check-public-boundary.sh index 7f1274b4..d694c4f3 100755 --- a/scripts/check-public-boundary.sh +++ b/scripts/check-public-boundary.sh @@ -6,6 +6,12 @@ cd "$root" status=0 +# Claude Code's personal local settings must never be tracked (.github#355). +if git ls-files -z | tr '\0' '\n' | grep -E '(^|/)\.claude/settings\.local\.json$'; then + echo 'refused: tracked .claude/settings.local.json' >&2 + exit 1 +fi + pattern_from_hex() { local hex="$1" local output="" diff --git a/tests/test_public_boundary_scanner.py b/tests/test_public_boundary_scanner.py index a356a8c6..140289bb 100644 --- a/tests/test_public_boundary_scanner.py +++ b/tests/test_public_boundary_scanner.py @@ -105,3 +105,47 @@ def test_local_identity_commit_is_rejected(self, scratch_repo: Path) -> None: assert result.returncode == 1, (result.stdout, result.stderr) assert "forbidden commit metadata" in result.stderr assert sha[:12] in result.stderr + + @pytest.mark.parametrize( + "path", + [".claude/settings.local.json", "component/.claude/settings.local.json"], + ) + def test_tracked_claude_local_settings_are_refused(self, scratch_repo: Path, path: str) -> None: + # durable-workflow/.github#355: refuse the path, never print the content. + target = scratch_repo / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("canary-value-must-not-be-printed\n", encoding="utf-8") + _git(scratch_repo, "add", "-f", path) + _git(scratch_repo, "commit", "-q", "-m", "Track local settings") + _commit(scratch_repo, "hello.txt", "unrelated\n") + + result = _run_scanner(scratch_repo, "-1 HEAD") + + assert result.returncode == 1, (result.stdout, result.stderr) + assert path in result.stdout + assert "refused: tracked .claude/settings.local.json" in result.stderr + assert "canary-value-must-not-be-printed" not in result.stdout + result.stderr + + def test_claude_local_settings_near_names_pass(self, scratch_repo: Path) -> None: + for path in ( + ".claude/settings.json", + ".claude/settings.local.json.example", + "component/settings.local.json", + ): + target = scratch_repo / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("{}\n", encoding="utf-8") + _git(scratch_repo, "add", path) + _git(scratch_repo, "commit", "-q", "-m", "Add near names") + + result = _run_scanner(scratch_repo, "-1 HEAD") + + assert result.returncode == 0, (result.stdout, result.stderr) + + def test_claude_local_settings_are_ignored(self, scratch_repo: Path) -> None: + shutil.copy2(REPO_ROOT / ".gitignore", scratch_repo / ".gitignore") + for path in (".claude/settings.local.json", "component/.claude/settings.local.json"): + target = scratch_repo / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("{}\n", encoding="utf-8") + _git(scratch_repo, "check-ignore", "-q", path)