You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Every run_skill dispatch on the codex backend crashes unconditionally since commit bfc5d2c70 (PR #4070, closes #4001, shipped v0.10.760+):
RuntimeError: Cannot dispatch skill 'make-plan' on backend 'codex': HOOK_REGISTRY contains fix-required entries [Read|Write|Edit|Bash|Grep|Glob] that cannot be enforced by this backend.
Observed in production run impl-20260611-163753-197057 (2026-06-11 16:37 PDT, installed v0.10.763, token-reserve project). Same-day regression: the identical order dispatched make-plan successfully on v0.10.751. The only bypass is a step-level non-Anthropic provider profile carrying ANTHROPIC_BASE_URL (reroutes to the claude-code backend object, tools_execution.py:744-758).
Root cause mechanics
_check_backend_compat → _get_fix_required_hook_matchers (src/autoskillit/server/tools/tools_execution.py:90-164) computes frozenset({"skill_load_guard"}).issubset(frozenset({"write_guard"})) → False → SkillResult.crashed() for every codex run_skill. Inputs:
Why "fix-required" is wrong and the gate protects nothing on codex
The runtime guard already self-bypasses on codex: skill_load_guard.py:103-104 exits 0 when its stem is absent from AUTOSKILLIT_APPLICABLE_GUARDS (codex sets write_guard). The session the gate blocks would run identically with the guard present-but-inert.
Codex has no Skill tool — the guard's entire premise (deny native tools until Skill loads SKILL.md context, PR Hook-Based Skill Loading Guard for Non-Anthropic Providers + Directive SKILL.md Descriptions #1937, commit 3eec9faee 2026-05-05) is structurally inapplicable. The release-flag writer skill_load_post_hook is already codex_status="not-applicable" (hook_registry.py:290-296), so the guard could never release even if registered.
The matcher names Claude tools (Read/Bash/Grep); codex tool names are read_file/shell/grep.
Reclassify: src/autoskillit/hook_registry.py:301 — change codex_status="fix-required" to codex_status="not-applicable", with a registry comment citing the evidence above (no Skill tool on codex; release flag unwritable; Claude-specific matcher; upstream read-path hook events do not exist). The Literal type at hook_registry.py:30 already admits "not-applicable". _SKIP_CODEX_STATUSES (execution/backends/_codex_hooks.py:24) treats both statuses identically, so codex config.toml generation is unchanged. Two co-required documentation edits in the same file:
Update the Codex Compatibility Table row at hook_registry.py:82 from skill_load_guard | fix-required to | not-applicable (the table is the human-readable status reference; leaving it stale contradicts the code).
Extend the status taxonomy comment at hook_registry.py:49 — the current definition ("Hook targets a tool/event that Codex does not support") only partially fits, since codex has a Bash-equivalent (shell); add a clause noting not-applicable also covers hooks whose required hook events are permanently non-firing on the backend. Alternative: introduce an explicit "unenforceable-waived" literal if maintainers prefer to preserve the distinction "backend has the failure mode but no enforcement exists" — either resolution is acceptable, but the chosen one must update the taxonomy comment, and a permanently-unsatisfiable fix-required feeding a crash gate must not remain.
Sync the registry hash (REQUIRED — without it CI fails): _canonical_registry_payload serializes codex_status for every hook (hook_registry.py:413-438, field at line 420), so this change alters HOOK_REGISTRY_HASH. test_committed_registry_hash_matches_live_registry (tests/hooks/test_hook_executability.py:81-98) asserts the committed src/autoskillit/hooks/registry.sha256 matches the live hash. After editing the registry, run task sync-hooks-hash (Taskfile.yml:276) and commit the updated registry.sha256.
Real-registry dispatch contract test (the test that would have caught this at the PR): in tests/arch/test_capability_consistency.py, add a test class parametrized over real BACKEND_REGISTRY (no monkeypatching), asserting _get_fix_required_hook_matchers(backend.capabilities.applicable_guards) == [] for every backend not listed in a new explicit, documented DISPATCH_INCAPABLE_BACKENDS: frozenset[str] allowlist (test-local module constant; empty after the reclassification). Pure module-level constants, xdist-safe. These changes MUST land together: with the old status the test fails on codex; with the new status the allowlist stays empty. Note: post-fix the assertion holds vacuously (zero fix-required entries remain) — that is the point: the test's value is as a regression guard asserting "every registered backend must have zero uncovered fix-required hooks," and the allowlist exists to force future exceptions to be explicit rather than silent.
Optional hardening: (a) add _EXPECTED_NOT_APPLICABLE and _EXPECTED_FIX_REQUIRED (expected: empty set) snapshot-set tests alongside _EXPECTED_DEGRADED (tests/hooks/test_hook_registry.py:380-392) so future status reclassifications are deliberate, not silent — this also compensates for test_generate_codex_hooks_config_excludes_fix_required (tests/hooks/test_hook_registry_codex_status.py:30-40) becoming vacuous once the fix-required set is empty; (b) refresh the stale "fix-required hook dispatch gate" phrase in the tools_execution.py size-budget description at tests/arch/test_subpackage_isolation.py:978-979.
Test impact (validated, then adversarially re-validated)
WILL FAIL without Change 2: test_committed_registry_hash_matches_live_registry (tests/hooks/test_hook_executability.py:81-98) — run task sync-hooks-hash.
TestHookFixRequiredDispatchGate (tests/server/test_run_skill_backend_compat.py:197-401): all five tests monkeypatch HOOK_REGISTRY — unaffected.
test_generate_codex_hooks_config_excludes_fix_required / _excludes_not_applicable (tests/hooks/test_hook_registry_codex_status.py): read the registry dynamically — still pass (the former becomes vacuous; see Change 4a).
test_pretooluse_deny_mechanism_is_set: exempts not-applicable entries — still passes, but silently stops checking this entry's mechanism field (accepted minor coverage loss; Change 4a's snapshot tests compensate).
test_capabilities_applicable_guards (tests/execution/backends/test_codex_backend.py:134-135) and test_commands_skill_session.py:75: assert applicable_guards fields, which this change does not touch — unaffected.
Claude-code and non-Anthropic-provider protection unchanged: CLAUDE_CODE_CAPABILITIES.applicable_guards (core/types/_type_backend.py:220) untouched; the guard still enforces where declared.
Known limitation (document, do not fix here)
_get_fix_required_hook_matchers consults the codex-specific codex_status field for whatever backend is being dispatched (tools_execution.py:90-100 takes only applicable_guards). For any future third backend the gate would evaluate CODEX statuses — a category error that currently passes only by accident. The contract test in Change 3 inherits this shape. The long-term fix (per-backend status maps or a renamed dispatch_gate_status) is out of scope here but should be recorded as a known limitation in the registry comment.
hook_registry.py skill_load_guard entry no longer has codex_status="fix-required"; the registry comment documents the permanent-unenforceability evidence with upstream issue links; the compatibility table row (line 82) and the status taxonomy comment (line ~49) are updated consistently with the chosen status.
task sync-hooks-hash has been run and the updated src/autoskillit/hooks/registry.sha256 is committed; test_committed_registry_hash_matches_live_registry passes.
A non-monkeypatched contract test iterates real BACKEND_REGISTRY × real HOOK_REGISTRY and fails if any backend outside DISPATCH_INCAPABLE_BACKENDS has uncovered fix-required hooks; DISPATCH_INCAPABLE_BACKENDS is empty.
run_skill dispatch on the codex backend passes _check_backend_compat (verifiable with a real CodexBackend instance and the real registry).
_EXPECTED_NOT_APPLICABLE and _EXPECTED_FIX_REQUIRED snapshot tests added (or explicitly declined with rationale).
Codex config.toml hook generation output is byte-identical before/after (status remains in _SKIP_CODEX_STATUSES).
Parallel-safety note for triage
Implementable in parallel with the companion open_kitchen preflight issue #4083 — zero file overlap (this issue: hook_registry.py, hooks/registry.sha256, tests/arch/, tests/hooks/; companion: server/tools/tools_kitchen.py, tests/server/).
Investigation
Prior investigation completed interactively (deep mode: code-path trace, git/design-intent timeline, test-gap analysis, adversarial challenge round, external research on Codex CLI hook capabilities, 2 post-report validators), followed by a 2-agent adversarial validation round on this issue body whose evidence was independently re-verified against the codebase (registry-hash impact, comment-table row, #3800/#4018/#4040 interactions). Full report at .autoskillit/temp/investigate/investigation_codex_fix_required_dispatch_gate_2026-06-11_171934.md in the development checkout.
Failure
Every
run_skilldispatch on the codex backend crashes unconditionally since commitbfc5d2c70(PR #4070, closes #4001, shipped v0.10.760+):Observed in production run
impl-20260611-163753-197057(2026-06-11 16:37 PDT, installed v0.10.763, token-reserve project). Same-day regression: the identical order dispatched make-plan successfully on v0.10.751. The only bypass is a step-level non-Anthropic provider profile carryingANTHROPIC_BASE_URL(reroutes to the claude-code backend object,tools_execution.py:744-758).Root cause mechanics
_check_backend_compat→_get_fix_required_hook_matchers(src/autoskillit/server/tools/tools_execution.py:90-164) computesfrozenset({"skill_load_guard"}).issubset(frozenset({"write_guard"}))→False→SkillResult.crashed()for every codexrun_skill. Inputs:HookDef(matcher=r"Read|Write|Edit|Bash|Grep|Glob", scripts=["guards/skill_load_guard.py"], session_scope="headless_only", codex_status="fix-required", mechanism="deny")—src/autoskillit/hook_registry.py:297-303CodexBackend.capabilities.applicable_guards = frozenset({"write_guard"})—src/autoskillit/execution/backends/codex.py:500Why "fix-required" is wrong and the gate protects nothing on codex
skill_load_guard.py:103-104exits 0 when its stem is absent fromAUTOSKILLIT_APPLICABLE_GUARDS(codex setswrite_guard). The session the gate blocks would run identically with the guard present-but-inert.Skilltool — the guard's entire premise (deny native tools until Skill loads SKILL.md context, PR Hook-Based Skill Loading Guard for Non-Anthropic Providers + Directive SKILL.md Descriptions #1937, commit3eec9faee2026-05-05) is structurally inapplicable. The release-flag writerskill_load_post_hookis alreadycodex_status="not-applicable"(hook_registry.py:290-296), so the guard could never release even if registered.read_file/shell/grep.read_file/greptool handlers never emit PreToolUse hook events ([Feature request] Extend PreToolUse hooks beyond Bash + implement updatedInput rewrite openai/codex#18491, Inconsistent PreToolUse hook coverage across tool handlers (most tools never emit hook events) openai/codex#20204 — both OPEN as of 2026-06-11, no ETA; official docs call hooks "a guardrail rather than a complete enforcement boundary"). The waiver is permanent, not temporary — "fix-required" can never transition to "works-as-is" via upstream fixes currently in flight.Changes
src/autoskillit/hook_registry.py:301— changecodex_status="fix-required"tocodex_status="not-applicable", with a registry comment citing the evidence above (no Skill tool on codex; release flag unwritable; Claude-specific matcher; upstream read-path hook events do not exist). TheLiteraltype athook_registry.py:30already admits"not-applicable"._SKIP_CODEX_STATUSES(execution/backends/_codex_hooks.py:24) treats both statuses identically, so codex config.toml generation is unchanged. Two co-required documentation edits in the same file:hook_registry.py:82fromskill_load_guard | fix-requiredto| not-applicable(the table is the human-readable status reference; leaving it stale contradicts the code).hook_registry.py:49— the current definition ("Hook targets a tool/event that Codex does not support") only partially fits, since codex has a Bash-equivalent (shell); add a clause notingnot-applicablealso covers hooks whose required hook events are permanently non-firing on the backend. Alternative: introduce an explicit"unenforceable-waived"literal if maintainers prefer to preserve the distinction "backend has the failure mode but no enforcement exists" — either resolution is acceptable, but the chosen one must update the taxonomy comment, and a permanently-unsatisfiablefix-requiredfeeding a crash gate must not remain._canonical_registry_payloadserializescodex_statusfor every hook (hook_registry.py:413-438, field at line 420), so this change altersHOOK_REGISTRY_HASH.test_committed_registry_hash_matches_live_registry(tests/hooks/test_hook_executability.py:81-98) asserts the committedsrc/autoskillit/hooks/registry.sha256matches the live hash. After editing the registry, runtask sync-hooks-hash(Taskfile.yml:276) and commit the updatedregistry.sha256.tests/arch/test_capability_consistency.py, add a test class parametrized over realBACKEND_REGISTRY(no monkeypatching), asserting_get_fix_required_hook_matchers(backend.capabilities.applicable_guards) == []for every backend not listed in a new explicit, documentedDISPATCH_INCAPABLE_BACKENDS: frozenset[str]allowlist (test-local module constant; empty after the reclassification). Pure module-level constants, xdist-safe. These changes MUST land together: with the old status the test fails on codex; with the new status the allowlist stays empty. Note: post-fix the assertion holds vacuously (zero fix-required entries remain) — that is the point: the test's value is as a regression guard asserting "every registered backend must have zero uncovered fix-required hooks," and the allowlist exists to force future exceptions to be explicit rather than silent._EXPECTED_NOT_APPLICABLEand_EXPECTED_FIX_REQUIRED(expected: empty set) snapshot-set tests alongside_EXPECTED_DEGRADED(tests/hooks/test_hook_registry.py:380-392) so future status reclassifications are deliberate, not silent — this also compensates fortest_generate_codex_hooks_config_excludes_fix_required(tests/hooks/test_hook_registry_codex_status.py:30-40) becoming vacuous once the fix-required set is empty; (b) refresh the stale "fix-required hook dispatch gate" phrase in thetools_execution.pysize-budget description attests/arch/test_subpackage_isolation.py:978-979.Test impact (validated, then adversarially re-validated)
test_committed_registry_hash_matches_live_registry(tests/hooks/test_hook_executability.py:81-98) — runtask sync-hooks-hash.TestHookFixRequiredDispatchGate(tests/server/test_run_skill_backend_compat.py:197-401): all five tests monkeypatchHOOK_REGISTRY— unaffected.test_generate_codex_hooks_config_excludes_fix_required/_excludes_not_applicable(tests/hooks/test_hook_registry_codex_status.py): read the registry dynamically — still pass (the former becomes vacuous; see Change 4a).test_pretooluse_deny_mechanism_is_set: exempts not-applicable entries — still passes, but silently stops checking this entry'smechanismfield (accepted minor coverage loss; Change 4a's snapshot tests compensate).test_capabilities_applicable_guards(tests/execution/backends/test_codex_backend.py:134-135) andtest_commands_skill_session.py:75: assertapplicable_guardsfields, which this change does not touch — unaffected.CLAUDE_CODE_CAPABILITIES.applicable_guards(core/types/_type_backend.py:220) untouched; the guard still enforces where declared.Known limitation (document, do not fix here)
_get_fix_required_hook_matchersconsults the codex-specificcodex_statusfield for whatever backend is being dispatched (tools_execution.py:90-100takes onlyapplicable_guards). For any future third backend the gate would evaluate CODEX statuses — a category error that currently passes only by accident. The contract test in Change 3 inherits this shape. The long-term fix (per-backend status maps or a renameddispatch_gate_status) is out of scope here but should be recorded as a known limitation in the registry comment.Process notes
crashedresult at dispatch time when the codex backend has anyfix-requiredhook entries in HOOK_REGISTRY, preventing silent degradation from hook-unenforceable sessions. #4001's acceptance criterion AC-5 ("the sole fix-required entry causes_check_backend_compatto return a crashed result for codex"). That AC encoded a permanently-unsatisfiable requirement as a crash; PR Implementation Plan: Surface a StructuredcrashedResult at Dispatch for Fix-Required Hooks #4070 implemented it faithfully — the defect is in the spec, not the implementation. Reference this issue as the superseding decision.test_codex_applicable_guards_is_empty_frozensetassertingCodexBackend.capabilities.applicable_guards == frozenset()— that test would FAIL against current code (codex.py:500isfrozenset({"write_guard"}), locked bytest_codex_backend.py:135), and even if the value were changed tofrozenset()it would not alter the gate outcome. Do not implement its prescribed test.skill_load_guard.pykeyed onAUTOSKILLIT_AGENT_BACKEND. No file overlap with this issue; after this reclassification the hook never registers on codex, so T5-P4-A3-WP1 Eliminate the structural defect where write_guard attempts PreToolUse deny decisions for codex sessions even though codex's PreToolUse does not fire for apply_patch or MCP calls. The codex backend ... #4018's early-exit becomes redundant defense-in-depth (harmless — its tests use synthetic env combinations and still pass).enforcement_strengthto HookDef) requirescodex_status == 'not-applicable'iffenforcement_strength['codex'] == 'not-applicable'(TestCodexStatusStrengthCoherence). When P5-A6-WP1 lands,skill_load_guardmust be populated withenforcement_strength={'codex': 'not-applicable', ...}to stay coherent with this change. Nothing to do in this issue; flagging for the P5-A6 implementer.Acceptance criteria
hook_registry.pyskill_load_guard entry no longer hascodex_status="fix-required"; the registry comment documents the permanent-unenforceability evidence with upstream issue links; the compatibility table row (line 82) and the status taxonomy comment (line ~49) are updated consistently with the chosen status.task sync-hooks-hashhas been run and the updatedsrc/autoskillit/hooks/registry.sha256is committed;test_committed_registry_hash_matches_live_registrypasses.BACKEND_REGISTRY× realHOOK_REGISTRYand fails if any backend outsideDISPATCH_INCAPABLE_BACKENDShas uncovered fix-required hooks;DISPATCH_INCAPABLE_BACKENDSis empty.run_skilldispatch on the codex backend passes_check_backend_compat(verifiable with a realCodexBackendinstance and the real registry)._EXPECTED_NOT_APPLICABLEand_EXPECTED_FIX_REQUIREDsnapshot tests added (or explicitly declined with rationale)._SKIP_CODEX_STATUSES).Parallel-safety note for triage
Implementable in parallel with the companion open_kitchen preflight issue #4083 — zero file overlap (this issue:
hook_registry.py,hooks/registry.sha256,tests/arch/,tests/hooks/; companion:server/tools/tools_kitchen.py,tests/server/).Investigation