From e141e617ecb24459cc4d13594ce0ca769a633160 Mon Sep 17 00:00:00 2001 From: Yue Ma Date: Thu, 10 Sep 2026 13:52:46 -0400 Subject: [PATCH] fix(install): say when Codex will not load the hooks it just registered --check-hooks reported "Registration and runtime files match" on a Codex project that had never been trusted, while Codex was loading zero hooks. Demonstrated: the registration is byte-perfect, the runtime files match, and hooks/list returns nothing at all. The message did carry a general caveat about verifying trust through /hooks, but a reader who has just been told their registration matches has no reason to go looking, and the failure is silent on the host side too. doctor now looks for a [projects.""] entry in the config under CODEX_HOME and, when it is absent, says so and names the file to add it to. Claude Code has no equivalent level and is never told this. It reports and changes nothing. It grants no trust, writes no configuration, and blocks nothing, which is the only shape appropriate here: the user must stay able to trust the project however they prefer, or not at all. Mutation check: removing the notice fails the test that asserts it appears while untrusted and disappears once trusted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016cCs5DdUjLDkP8V3Z5x6XH --- scripts/hook_install.py | 26 ++++++++++++++++++++++++++ scripts/test_hook_presets.py | 17 +++++++++++++++++ 2 files changed, 43 insertions(+) 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: