diff --git a/launchd/com.brainlayer.jsonl-backup.plist b/launchd/com.brainlayer.jsonl-backup.plist
index 8ca8d008..63d161cf 100644
--- a/launchd/com.brainlayer.jsonl-backup.plist
+++ b/launchd/com.brainlayer.jsonl-backup.plist
@@ -10,8 +10,11 @@
ProgramArguments
- /usr/bin/env
- python3
+
+ /opt/homebrew/opt/brainlayer/libexec/venv/bin/python
-m
brainlayer.jsonl_backup
diff --git a/tests/test_hook_python.py b/tests/test_hook_python.py
index 9463a2cc..2c9477fb 100644
--- a/tests/test_hook_python.py
+++ b/tests/test_hook_python.py
@@ -506,3 +506,40 @@ def test_installed_brainlayer_hooks_are_pinned():
assert findings == [], "BrainLayer hooks still resolve their interpreter through PATH: " + "; ".join(
f"{f.event}: {f.command}" for f in findings
)
+
+
+def test_launchd_plist_templates_pin_their_interpreter():
+ """launchd templates must not let PATH choose the interpreter.
+
+ #790 pinned the hooks. The nightly backup template was missed and still shipped
+ `/usr/bin/env python3`, which fronts the framework python whose `_brainlayer.pth`
+ injects `~/Gits/brainlayer/src` -- so a `git checkout` at the repo root would have
+ silently re-aimed the job that PREVENTS data loss at a working tree.
+
+ Found by the PR #815 pair review. Applying the same affirmative gate the hooks use:
+ anything this cannot vouch for is a finding, not a pass.
+ """
+ import plistlib
+ from pathlib import Path
+
+ from brainlayer.hook_python import is_pinned_interpreter
+
+ repo_root = Path(__file__).resolve().parents[1]
+ plists = sorted((repo_root / "launchd").glob("*.plist"))
+ assert plists, "expected launchd templates to exist"
+
+ unpinned: list[str] = []
+ for path in plists:
+ args = plistlib.loads(path.read_bytes()).get("ProgramArguments") or []
+ if not args:
+ continue
+ interpreter = args[0]
+ # A wrapper script is not an interpreter claim; only judge direct python invocations.
+ if "python" not in interpreter and not interpreter.endswith("/env"):
+ continue
+ if interpreter.endswith("/env"):
+ interpreter = f"{interpreter} {args[1] if len(args) > 1 else ''}".strip()
+ if not is_pinned_interpreter(interpreter):
+ unpinned.append(f"{path.name}: {interpreter}")
+
+ assert not unpinned, "launchd templates must name a pinned interpreter, not PATH: " + "; ".join(unpinned)