Surfaced while drafting the adoption fixture's runtime-registration payloads
(REG-01). Recorded rather than fixed there: the payload decision does not
depend on it, but the blind spot is real and outlives that decision.
What
init.sh tells every Codex adopter, in both registration blocks
(init.sh:2017 and init.sh:2137), to "Set [features].hooks to true".
kit_doctor.py grades that switch only when .codex/config.toml exists.
When the file is absent the switch is not reported at all — and a
.codex/hooks.json whose lifecycle forms all verify then renders as an
unqualified green report.
The two cases an operator most needs told apart — enabled and never
configured — are identical in the report. The third case, explicitly
disabled, is caught and exits 1.
Reproduction
Three disposable copies of the original adoption fixture (installed from kit
ab0a6d62308b, engines under scripts/devkit), each given the same
canonical .codex/hooks.json and differing only in .codex/config.toml.
Each copy's own installed doctor was run as
python3 <copy>/scripts/devkit/kit_doctor.py --root <copy> --manifest <comparison-source>/kit-manifest.json, cwd the copy, from
/Users/topi/Coding/agentic-dev-kit at
a4419dcd541f25162971615444aa995bc0dfb474 on 2026-09-06 UTC:
.codex/config.toml |
doctor line for the switch |
exit |
| absent |
(none) |
0 |
[features] hooks = false |
✗ … disabled by the project config |
1 |
[features] codex_hooks = false |
✗ … disabled by the project config |
1 |
In the absent case the report carries four green Codex lines — both engine
paths resolving and both canonical lifecycle form verified — with nothing
anywhere in it about [features].
Mechanism
Two independent decisions compose into the silence:
REGISTRATION_SURFACES (kit_doctor.py:1030) carries
("codex", ".codex/config.toml", False) — report_absent=False, so an
absent config.toml prints no line. That flag is right for its stated
purpose: the surface is optional as a registration source.
codex_documents (kit_doctor.py:1800, populated at :1919) only ever
holds surfaces that exist as files, so the features grading at
kit_doctor.py:1978 is guarded by
isinstance(codex_config, dict) and occurrence_names and never runs at all.
Neither is wrong alone. Together they mean the switch is graded only when the
adopter already wrote the file, which is the population least likely to have
left it unset.
Why it matters
The module's own comment above REGISTRATION_SURFACES gives the rule this
case fails: "'no registration here' and 'a registration that does not work'
look identical to an operator who never sees the hook fire" — which is exactly
why .claude/settings.json and .codex/hooks.json carry
report_absent=True. A verified hooks.json under an unset [features]
is the same shape one axis over, and #303 settled that these registrations
are hand-maintained forever, so the doctor's report is the only standing check.
The open question this does not settle
Whether an unset [features].hooks actually stops Codex loading
hooks.json is not established here, and nothing in this repository can
establish it. /hooks in a Codex session is the authority. Either answer
still leaves something to fix, which is why this is filed now:
- If unset means disabled, the doctor reports a dead registration as green.
- If unset means enabled, then
init.sh's instruction is unnecessary and
should be dropped rather than left telling adopters to set a switch that does
nothing.
Proposed fix
Report the un-set case as an advisory · line whenever a Codex lifecycle
registration was recognised and [features] did not resolve to a boolean
true — naming init.sh's instruction and /hooks as the authority, the way
the absent-surface lines already do. That is honest under either answer above
and does not require settling it.
Related: #303 (why registrations are hand-written), #363 (no test
executes a registration), #392 (the report's other silent-verdict axis).
What
init.shtells every Codex adopter, in both registration blocks(
init.sh:2017andinit.sh:2137), to "Set[features].hooksto true".kit_doctor.pygrades that switch only when.codex/config.tomlexists.When the file is absent the switch is not reported at all — and a
.codex/hooks.jsonwhose lifecycle forms all verify then renders as anunqualified green report.
The two cases an operator most needs told apart — enabled and never
configured — are identical in the report. The third case, explicitly
disabled, is caught and exits 1.
Reproduction
Three disposable copies of the original adoption fixture (installed from kit
ab0a6d62308b, engines underscripts/devkit), each given the samecanonical
.codex/hooks.jsonand differing only in.codex/config.toml.Each copy's own installed doctor was run as
python3 <copy>/scripts/devkit/kit_doctor.py --root <copy> --manifest <comparison-source>/kit-manifest.json, cwd the copy, from/Users/topi/Coding/agentic-dev-kitata4419dcd541f25162971615444aa995bc0dfb474on 2026-09-06 UTC:.codex/config.toml[features] hooks = false✗ … disabled by the project config[features] codex_hooks = false✗ … disabled by the project configIn the absent case the report carries four green Codex lines — both engine
paths resolving and both
canonical lifecycle form verified— with nothinganywhere in it about
[features].Mechanism
Two independent decisions compose into the silence:
REGISTRATION_SURFACES(kit_doctor.py:1030) carries("codex", ".codex/config.toml", False)—report_absent=False, so anabsent
config.tomlprints no line. That flag is right for its statedpurpose: the surface is optional as a registration source.
codex_documents(kit_doctor.py:1800, populated at:1919) only everholds surfaces that exist as files, so the
featuresgrading atkit_doctor.py:1978is guarded byisinstance(codex_config, dict) and occurrence_namesand never runs at all.Neither is wrong alone. Together they mean the switch is graded only when the
adopter already wrote the file, which is the population least likely to have
left it unset.
Why it matters
The module's own comment above
REGISTRATION_SURFACESgives the rule thiscase fails: "'no registration here' and 'a registration that does not work'
look identical to an operator who never sees the hook fire" — which is exactly
why
.claude/settings.jsonand.codex/hooks.jsoncarryreport_absent=True. A verifiedhooks.jsonunder an unset[features]is the same shape one axis over, and
#303settled that these registrationsare hand-maintained forever, so the doctor's report is the only standing check.
The open question this does not settle
Whether an unset
[features].hooksactually stops Codex loadinghooks.jsonis not established here, and nothing in this repository canestablish it.
/hooksin a Codex session is the authority. Either answerstill leaves something to fix, which is why this is filed now:
init.sh's instruction is unnecessary andshould be dropped rather than left telling adopters to set a switch that does
nothing.
Proposed fix
Report the un-set case as an advisory
·line whenever a Codex lifecycleregistration was recognised and
[features]did not resolve to a booleantrue — naming
init.sh's instruction and/hooksas the authority, the waythe absent-surface lines already do. That is honest under either answer above
and does not require settling it.
Related:
#303(why registrations are hand-written),#363(no testexecutes a registration),
#392(the report's other silent-verdict axis).