From 15217bdd06330b9189d47c25a5749fe197c3f570 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Mon, 9 Nov 2026 22:47:15 +0000 Subject: [PATCH] Refuse tracked personal agent settings before public content scans --- .github/workflows/public-boundary.yml | 3 + .gitignore | 4 ++ scripts/check-agent-local-settings.py | 37 +++++++++++ scripts/check-public-boundary.sh | 5 ++ tests/test_public_boundary.py | 91 +++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 scripts/check-agent-local-settings.py create mode 100644 tests/test_public_boundary.py diff --git a/.github/workflows/public-boundary.yml b/.github/workflows/public-boundary.yml index c8d5852afce..0ef471c0e63 100644 --- a/.github/workflows/public-boundary.yml +++ b/.github/workflows/public-boundary.yml @@ -19,6 +19,9 @@ jobs: with: fetch-depth: 0 + - name: Test tracked personal-settings refusal + run: python3 -I -B -m unittest discover -s tests -p test_public_boundary.py + - name: Scan files and commit metadata shell: bash run: | diff --git a/.gitignore b/.gitignore index 39b88293823..8e209f50993 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ yarn-error.log* docker* Docker* + +# Project-local personal agent settings +/.claude/settings.local.json +**/.claude/settings.local.json diff --git a/scripts/check-agent-local-settings.py b/scripts/check-agent-local-settings.py new file mode 100644 index 00000000000..d7fe5bd18c5 --- /dev/null +++ b/scripts/check-agent-local-settings.py @@ -0,0 +1,37 @@ +"""Refuse tracked operator-local settings without reading their contents.""" + +from __future__ import annotations + +import os +import subprocess +import sys + + +FORBIDDEN = b".claude/settings.local.json" + + +def git_output(*arguments: str) -> bytes: + return subprocess.run( + ["git", *arguments], + check=True, + stdout=subprocess.PIPE, + ).stdout + + +def main() -> int: + repository = os.fsdecode(git_output("rev-parse", "--show-toplevel").rstrip(b"\n")) + tracked = git_output("-C", repository, "ls-files", "-z", "--") + refused = False + for path in tracked.split(b"\0"): + if path == FORBIDDEN or path.endswith(b"/" + FORBIDDEN): + display = os.fsdecode(path) + print( + f"public-boundary: forbidden project-local settings path: {display!r}", + file=sys.stderr, + ) + refused = True + return int(refused) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-public-boundary.sh b/scripts/check-public-boundary.sh index e04289efe3e..465a272f279 100755 --- a/scripts/check-public-boundary.sh +++ b/scripts/check-public-boundary.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash set -euo pipefail +script_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" root="${1:-.}" cd "$root" +# Exact source from durable-workflow/.github d3b990dac1cb076b88c95e0768cf757f39c3ded2. +# Refuse paths before existing content scans can open a local settings file. +python3 -I -B "$script_root/check-agent-local-settings.py" + status=0 pattern_from_hex() { diff --git a/tests/test_public_boundary.py b/tests/test_public_boundary.py new file mode 100644 index 00000000000..30528d0c080 --- /dev/null +++ b/tests/test_public_boundary.py @@ -0,0 +1,91 @@ +"""Exercise the ordinary boundary entrypoint against isolated tracked trees.""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + +REPOSITORY = Path(__file__).resolve().parents[1] +BOUNDARY = REPOSITORY / "scripts/check-public-boundary.sh" +ENVIRONMENT = {"PATH": os.defpath, "LC_ALL": "C.UTF-8", "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"} + + +class PersonalSettingsBoundaryTest(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.addCleanup(self.directory.cleanup) + self.root = Path(self.directory.name) + self.git("init", "--quiet") + self.git("config", "user.name", "Boundary fixture") + self.git("config", "user.email", "boundary@example.test") + self.write(".gitignore", (REPOSITORY / ".gitignore").read_text()) + self.git("add", ".gitignore") + self.git("commit", "--quiet", "-m", "Initialize fixture") + + def git(self, *args): + return subprocess.run(["git", *args], cwd=self.root, env=ENVIRONMENT, check=True, capture_output=True, text=True) + + def write(self, name, content="canary-value-must-not-be-read\n"): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content) + return path + + def scan(self): + return subprocess.run(["bash", str(BOUNDARY), str(self.root)], env=ENVIRONMENT, capture_output=True, text=True) + + def test_tracked_paths_refuse_before_content_scans(self): + for name in [".claude/settings.local.json", "component/.claude/settings.local.json", "line\nbreak/.claude/settings.local.json", "node_modules/dependency/.claude/settings.local.json"]: + with self.subTest(name=name): + path = self.write(name) + self.git("add", "-f", "--", name) + path.chmod(0) + try: + result = self.scan() + finally: + path.chmod(0o600) + self.git("rm", "--cached", "--quiet", "--", name) + path.unlink() + self.assertEqual(1, result.returncode, result.stderr) + self.assertIn("forbidden project-local settings path:", result.stderr) + self.assertNotIn("canary-value-must-not-be-read", result.stdout + result.stderr) + self.assertNotIn("line\nbreak", result.stderr) + + def test_root_and_nested_untracked_settings_are_ignored(self): + for name in [".claude/settings.local.json", "component/.claude/settings.local.json"]: + self.write(name) + self.git("check-ignore", "--quiet", "--", name) + self.git("add", ".") + self.assertEqual("", self.git("ls-files", "--", "*.local.json").stdout) + result = self.scan() + self.assertEqual(0, result.returncode, result.stderr) + + def test_near_names_remain_allowed(self): + for name in [".claude/settings.json", ".claude/settings.local.json.example", "component/settings.local.json"]: + self.write(name, "{}\n") + self.git("add", "--", name) + result = self.scan() + self.assertEqual(0, result.returncode, result.stderr) + + def test_preexisting_tracked_file_refuses_after_unrelated_commit(self): + self.write("legacy/.claude/settings.local.json") + self.git("add", "-f", ".") + self.git("commit", "--quiet", "-m", "Historical tracked fixture") + self.write("README.md", "Unrelated documentation\n") + self.git("add", "README.md") + self.git("commit", "--quiet", "-m", "Unrelated update") + result = self.scan() + self.assertEqual(1, result.returncode) + self.assertIn("legacy/.claude/settings.local.json", result.stderr) + self.assertNotIn("canary-value-must-not-be-read", result.stdout + result.stderr) + + def test_existing_content_rule_still_refuses(self): + self.write("README.md", "zor" + "poration/fixture\n") + self.git("add", "README.md") + result = self.scan() + self.assertEqual(1, result.returncode) + self.assertIn("forbidden file content", result.stderr) + + +if __name__ == "__main__": + unittest.main()