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

Merged
sysadmin merged 1 commits from fix/issue-3-provenance-python-exec into master 2026-07-02 05:23:23 -05:00
3 changed files with 153 additions and 0 deletions
+32
View File
@@ -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`.
+61
View File
@@ -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"
+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()