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:
@@ -382,3 +382,35 @@ python3 -m pytest tests/ -v
|
|||||||
| `test_mirror_refs.py` | Flags, safety defaults, local integration tests |
|
| `test_mirror_refs.py` | Flags, safety defaults, local integration tests |
|
||||||
|
|
||||||
All tests mock network and keychain access — no real API calls are made.
|
All tests mock network and keychain access — no real API calls are made.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### macOS: `com.apple.provenance` blocks Python execution (#3)
|
||||||
|
|
||||||
|
On macOS Sequoia and later, files written by an agent/IDE terminal receive the
|
||||||
|
`com.apple.provenance` extended attribute, and macOS blocks `Python.app` from
|
||||||
|
**executing** such files. Symptoms: newly created/restored `.py` files fail to
|
||||||
|
run (e.g. `create_issue.py` "vanishing" or refusing to execute), while shell
|
||||||
|
scripts and files created before the session are unaffected. This is a macOS
|
||||||
|
security feature, not a bug in this project's code.
|
||||||
|
|
||||||
|
Workarounds (run from a terminal with **Full Disk Access**, e.g. `Terminal.app`
|
||||||
|
— not the IDE terminal, or the removal itself may be blocked):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Preferred: strip only com.apple.provenance under the repo (dry-run first)
|
||||||
|
./scripts/clear-provenance --dry-run
|
||||||
|
./scripts/clear-provenance
|
||||||
|
|
||||||
|
# Or a single file
|
||||||
|
./scripts/clear-provenance /path/to/file.py
|
||||||
|
|
||||||
|
# Manual equivalents
|
||||||
|
xattr -r -d com.apple.provenance /Users/jasonwalker/Development/Gitea-Tools/
|
||||||
|
xattr -cr /Users/jasonwalker/Development/Gitea-Tools/ # clears ALL xattrs
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, grant Full Disk Access to the terminal app in
|
||||||
|
**System Settings → Privacy & Security**. `scripts/clear-provenance` removes only
|
||||||
|
`com.apple.provenance` (leaving other extended attributes intact) and supports
|
||||||
|
`--dry-run`.
|
||||||
|
|||||||
Executable
+61
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# clear-provenance — strip the macOS com.apple.provenance extended attribute so
|
||||||
|
# Python.app can execute .py files created by agent/IDE terminals (issue #3).
|
||||||
|
#
|
||||||
|
# macOS Sequoia+ blocks Python.app from executing files carrying
|
||||||
|
# com.apple.provenance. Files written by the agent terminal get it; shell
|
||||||
|
# scripts are unaffected. This is a macOS security feature, not a bug in our
|
||||||
|
# code — see the Troubleshooting section of the README.
|
||||||
|
#
|
||||||
|
# Run from a terminal with Full Disk Access (e.g. Terminal.app), not the IDE
|
||||||
|
# terminal, or the removal itself may be blocked.
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
usage: scripts/clear-provenance [--dry-run] [path]
|
||||||
|
|
||||||
|
Recursively remove the com.apple.provenance extended attribute under <path>
|
||||||
|
(default: the repository root). macOS only. Only that attribute is removed;
|
||||||
|
other extended attributes are left intact.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
scripts/clear-provenance --dry-run
|
||||||
|
scripts/clear-provenance
|
||||||
|
scripts/clear-provenance /path/to/file.py
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
dry_run=0
|
||||||
|
while [[ "${1:-}" == --* ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run) dry_run=1 ;;
|
||||||
|
--help) usage; exit 0 ;;
|
||||||
|
*) usage >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $# -gt 1 ]]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
repo_root="$(cd "$script_dir/.." && pwd)"
|
||||||
|
target="${1:-$repo_root}"
|
||||||
|
|
||||||
|
if [[ ! -e "$target" ]]; then
|
||||||
|
printf 'clear-provenance: no such path: %s\n' "$target" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove only com.apple.provenance; tolerate files that do not carry it.
|
||||||
|
if [[ "$dry_run" -eq 1 ]]; then
|
||||||
|
printf 'clear-provenance: [dry-run] would run: xattr -r -d com.apple.provenance %q\n' "$target"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
xattr -r -d com.apple.provenance "$target" 2>/dev/null || true
|
||||||
|
printf 'clear-provenance: removed com.apple.provenance recursively under: %s\n' "$target"
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user