diff --git a/scripts/hook_install.py b/scripts/hook_install.py index 0647992..0230745 100644 --- a/scripts/hook_install.py +++ b/scripts/hook_install.py @@ -120,6 +120,31 @@ def version(host): return ".".join(map(str, actual)) +def codex_home(): + return Path(os.environ.get("CODEX_HOME") or Path.home() / ".codex") + + +def untrusted_project(host, project): + """Codex loads no project-local hooks until the project is trusted in its own + config, and says nothing at install time. A -c override does not satisfy it. + + This only reports; it changes no trust and blocks nothing.""" + if host != "codex": + return "" + path = codex_home() / "config.toml" + try: + data = path.read_bytes() if path.is_file() else b"" + except OSError: + return "" + if f'[projects."{project}"]'.encode() in data: + return "" + return ( + f" Codex has not been told to trust {project}: add a " + f'[projects."{project}"] entry with trust_level to {path}, or trust it ' + "through the host. Until then it loads no project-local hooks at all." + ) + + def locations(host, project): project = Path(project).expanduser().resolve(strict=True) result = subprocess.run( @@ -698,4 +723,5 @@ def doctor(host, project): f"Installed presets: {', '.join(presets)}. Skill version {installed}. " f"Registration and runtime files match ({host} {version_string}). {portability} " "Trust/enabled state and real host delivery still require /hooks verification." + + untrusted_project(host, project) ) diff --git a/scripts/test_hook_presets.py b/scripts/test_hook_presets.py index 1836853..01f669d 100644 --- a/scripts/test_hook_presets.py +++ b/scripts/test_hook_presets.py @@ -3,6 +3,7 @@ import copy import hashlib import json +import os import shlex import sys from pathlib import Path @@ -160,6 +161,22 @@ def test_adding_a_preset_does_not_silently_rewrite_an_existing_shape(self): hooks.remove(host, self.project, presets=["checkpoints"]) self.assertEqual(json.loads(receipt.read_bytes())["schema"], 2) + def test_check_hooks_says_when_codex_will_not_load_the_hooks(self): + """It reported a healthy registration while Codex loaded zero hooks.""" + home = self.root / "codex-home" + home.mkdir() + with patch.dict(os.environ, {"CODEX_HOME": str(home)}): + self.install("codex", ["continuity"]) + self.assertIn("has not been told to trust", hooks.doctor("codex", self.project)) + (home / "config.toml").write_text( + f'[projects."{self.project}"]\ntrust_level = "trusted"\n') + self.assertNotIn("has not been told to trust", + hooks.doctor("codex", self.project)) + # Claude Code has no equivalent level, so it must never be told this. + self.install("claude-code", ["continuity"]) + self.assertNotIn("has not been told to trust", + hooks.doctor("claude-code", self.project)) + def test_check_hooks_names_a_changed_interpreter(self): """A groups mismatch caused by the interpreter must say so, not blame paths.""" for host in hooks.PATHS: