Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions scripts/hook_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
)
17 changes: 17 additions & 0 deletions scripts/test_hook_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import copy
import hashlib
import json
import os
import shlex
import sys
from pathlib import Path
Expand Down Expand Up @@ -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:
Expand Down
Loading