fix: document + tool the macOS com.apple.provenance workaround (#3)

Root cause: macOS Sequoia+ blocks Python.app from executing files carrying the
com.apple.provenance extended attribute. Files written by an agent/IDE terminal
get it (shell scripts and pre-session files do not). This is a macOS security
feature, not a bug in our code — so the fix is an operator workaround, not a
code change to the tools.

- scripts/clear-provenance: recursively removes ONLY com.apple.provenance under
  a path (default: repo root); tolerates files without it; leaves other xattrs
  intact; supports --dry-run. Advises running from a Full-Disk-Access terminal.
- README Troubleshooting section documenting the symptom, the helper, manual
  xattr equivalents, and the Full Disk Access alternative.

Narrow + macOS-specific; no auth/release/worktree/tracker/MCP behavior changed.

Tests: tests/test_clear_provenance.py (6 cases) — dry-run default/explicit path,
missing-path error, bad-flag/too-many-args exit 2, and that only
com.apple.provenance is targeted (not a blanket xattr clear). Dry-run only; no
real xattr mutation.

bash -n clean; py_compile mcp_server.py clean; full suite 319 passed / 0
failures; git diff --check clean; no secrets.

Closes #3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 06:13:26 -04:00
parent 3a246ab553
commit e842b60ad8
3 changed files with 153 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
"""Tests for scripts/clear-provenance (#3).
Exercises argument handling and the inert --dry-run path only — no real xattr
mutation, no network. (Actually removing com.apple.provenance is macOS-only and
has real side effects, so it is not exercised here.)
"""
import subprocess
import tempfile
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SCRIPT = REPO / "scripts" / "clear-provenance"
def run(*args):
proc = subprocess.run(["bash", str(SCRIPT), *args],
capture_output=True, text=True, cwd=str(REPO))
return proc.returncode, proc.stdout, proc.stderr
class TestClearProvenance(unittest.TestCase):
def test_dry_run_defaults_to_repo_root(self):
rc, out, _ = run("--dry-run")
self.assertEqual(rc, 0)
self.assertIn("would run: xattr -r -d com.apple.provenance", out)
self.assertIn(str(REPO), out)
def test_dry_run_explicit_path(self):
with tempfile.TemporaryDirectory() as d:
f = Path(d) / "x.py"
f.write_text("print('hi')\n")
rc, out, _ = run("--dry-run", str(f))
self.assertEqual(rc, 0)
self.assertIn(str(f), out)
def test_missing_path_errors(self):
rc, _, err = run("--dry-run", "/no/such/path-xyz")
self.assertEqual(rc, 1)
self.assertIn("no such path", err)
def test_bad_flag_exit_2(self):
rc, _, _ = run("--bogus")
self.assertEqual(rc, 2)
def test_too_many_args_exit_2(self):
rc, _, _ = run("a", "b")
self.assertEqual(rc, 2)
def test_only_targets_provenance_attribute(self):
# The command removes only com.apple.provenance, not all xattrs.
rc, out, _ = run("--dry-run")
self.assertIn("com.apple.provenance", out)
self.assertNotIn("xattr -rc", out) # not a blanket "clear all"
self.assertNotIn("-c ", out)
if __name__ == "__main__":
unittest.main()